Files
andros 2786f7b1bb Test poll factory emits LANG/TAGS/MOOD/VISIBILITY
The poll signature grew to accept lang/tags/mood/visibility a while back so
the Compose view could pass them through to NewPostOptions.poll, but nothing
pinned that behaviour. Two tests:

- With all four provided, the generated block contains :LANG:, :TAGS:,
  :MOOD:, :VISIBILITY: alongside the existing :POLL_END:.
- With none provided, those properties are absent (so the default call
  site keeps producing minimal poll blocks).
2026-04-24 10:36:54 +02:00

238 lines
9.6 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 Foundation
import Testing
@testable import OrgSocialKit
private let feed = """
#+TITLE: Test
#+NICK: test
* Posts
"""
private let feedURL = URL(string: "https://example.com/social.org")!
private let relay = URL(string: "https://relay.org-social.org")!
// Poll post with live votes on relay (andros Nov 2025)
private let livePollURL = "https://host.org-social.org/andros/social.org#2025-11-26T09:03:48+0100"
@Suite("PostWriter boost block")
struct BoostBlockTests {
let writer = PostWriter()
@Test("boost factory creates INCLUDE property")
func boostFactory() {
let opts = NewPostOptions.boost(of: "https://shom.dev/social.org#2025-09-06T22:36:20-0500")
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains(":INCLUDE: https://shom.dev/social.org#2025-09-06T22:36:20-0500"))
}
@Test("boost with commentary includes body text")
func boostWithCommentary() {
let opts = NewPostOptions.boost(of: "https://shom.dev/social.org#2025-09-06T22:36:20-0500", commentary: "Great post!")
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains("Great post!"))
#expect(block.contains(":INCLUDE:"))
}
@Test("pure boost (no commentary) has no body text")
func pureBoostNoBody() throws {
let opts = NewPostOptions.boost(of: "https://shom.dev/social.org#2025-09-06T22:36:20-0500")
let (updated, _) = try writer.appendPost(to: feed, feedURL: feedURL, options: opts)
// The block must not contain any non-empty lines after :END:
let lines = updated.components(separatedBy: "\n")
let endIdx = lines.lastIndex { $0.trimmingCharacters(in: .whitespaces) == ":END:" } ?? 0
let bodyLines = lines[(endIdx + 1)...].filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
#expect(bodyLines.isEmpty)
}
}
@Suite("PostWriter poll block")
struct PollBlockTests {
let writer = PostWriter()
@Test("poll factory creates POLL_END property")
func pollHasPollEnd() {
let end = Date(timeIntervalSinceNow: 7 * 86400)
let opts = NewPostOptions.poll(question: "Which editor?", options: ["Emacs", "Vim"], end: end)
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains(":POLL_END:"))
}
@Test("poll question and options are separated by a blank line")
func pollBlankLineBetweenQuestionAndOptions() {
let end = Date(timeIntervalSinceNow: 86400)
let opts = NewPostOptions.poll(question: "Pick one", options: ["A"], end: end)
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains("Pick one\n\n- [ ] A"))
}
@Test("poll body contains checkboxes for each option")
func pollCheckboxes() {
let end = Date(timeIntervalSinceNow: 86400)
let opts = NewPostOptions.poll(question: "Pick one", options: ["A", "B", "C"], end: end)
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains("- [ ] A"))
#expect(block.contains("- [ ] B"))
#expect(block.contains("- [ ] C"))
}
@Test("poll vote creates POLL_OPTION property")
func pollVoteBlock() {
let opts = NewPostOptions.vote(for: "Emacs", on: "https://shom.dev/social.org#2025-01-01T00:00:00Z")
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains(":POLL_OPTION: Emacs"))
#expect(block.contains(":REPLY_TO: https://shom.dev/social.org#2025-01-01T00:00:00Z"))
}
@Test("vote block has no body text")
func voteNoBody() throws {
let opts = NewPostOptions.vote(for: "Emacs", on: "https://example.com/social.org#2025-01-01T00:00:00Z")
let (updated, _) = try writer.appendPost(to: feed, feedURL: feedURL, options: opts)
let lines = updated.components(separatedBy: "\n")
let endIdx = lines.lastIndex { $0.trimmingCharacters(in: .whitespaces) == ":END:" } ?? 0
let bodyLines = lines[(endIdx + 1)...].filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
#expect(bodyLines.isEmpty)
}
@Test("poll factory emits LANG, TAGS, MOOD, VISIBILITY when provided")
func pollAcceptsSharedFields() {
let end = Date(timeIntervalSinceNow: 86400)
let opts = NewPostOptions.poll(
question: "Pick one",
options: ["A", "B"],
end: end,
lang: "es",
tags: "poll survey",
mood: "🗳",
visibility: .mention
)
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains(":LANG: es"))
#expect(block.contains(":TAGS: poll survey"))
#expect(block.contains(":MOOD: 🗳"))
#expect(block.contains(":VISIBILITY: mention"))
#expect(block.contains(":POLL_END:"))
}
@Test("poll factory omits LANG/TAGS/MOOD/VISIBILITY when not provided")
func pollOmitsAbsentSharedFields() {
let end = Date(timeIntervalSinceNow: 86400)
let opts = NewPostOptions.poll(question: "Pick one", options: ["A"], end: end)
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(!block.contains(":LANG:"))
#expect(!block.contains(":TAGS:"))
#expect(!block.contains(":MOOD:"))
#expect(!block.contains(":VISIBILITY:"))
}
}
@Suite("PostWriter migration block")
struct MigrationBlockTests {
let writer = PostWriter()
@Test("migration factory creates MIGRATION property")
func migrationBlock() {
let opts = NewPostOptions.migration(from: "https://old.example.com/social.org",
to: "https://new.example.com/social.org")
let block = writer.buildPostBlock(timestamp: "2025-01-01T12:00:00+01:00", options: opts)
#expect(block.contains(":MIGRATION: https://old.example.com/social.org https://new.example.com/social.org"))
}
}
@Suite("PostWriter scheduled post")
struct ScheduledPostTests {
let writer = PostWriter()
@Test("appendPost with future date uses that timestamp")
func futureTimestamp() throws {
let future = Date(timeIntervalSinceNow: 3600)
let opts = NewPostOptions(text: "Scheduled post")
let (_, postURL) = try writer.appendPost(to: feed, feedURL: feedURL, options: opts, date: future)
let timestamp = postURL.components(separatedBy: "#").last ?? ""
// The timestamp must parse to a future date (parse helper accepts
// both compact and colon forms, matching the spec).
let parsed = PostWriter.parseTimestamp(timestamp)
#expect(parsed != nil)
#expect(parsed! > Date())
}
@Test("generateTimestamp emits compact timezone form (no colon)")
func timestampCompactForm() {
let ts = writer.generateTimestamp(for: Date(timeIntervalSince1970: 0))
// Last 5 chars must be `±HHMM` (compact) or exactly match `Z` is
// disallowed by the new emission contract we always emit `±HHMM`.
let suffix = String(ts.suffix(5))
let matches = suffix.range(of: #"^[+-]\d{4}$"#, options: .regularExpression) != nil
#expect(matches, "expected compact timezone, got: \(ts)")
}
@Test("scheduled post block contains future timestamp in heading")
func futureTimestampInHeading() throws {
let future = Date(timeIntervalSinceNow: 3600)
let opts = NewPostOptions(text: "Will post later")
let (updated, _) = try writer.appendPost(to: feed, feedURL: feedURL, options: opts, date: future)
let generatedTimestamp = writer.generateTimestamp(for: future)
#expect(updated.contains("** \(generatedTimestamp)"))
}
}
@Suite("RelayClient poll votes", .serialized)
struct PollVotesTests {
let client = RelayClient()
@Test("fetchPollVotes returns options for known poll")
func fetchVotesNonEmpty() async throws {
let votes = try await client.fetchPollVotes(for: livePollURL, from: relay)
#expect(!votes.isEmpty)
}
@Test("fetchPollVotes options have non-empty names")
func voteOptionNames() async throws {
let votes = try await client.fetchPollVotes(for: livePollURL, from: relay)
for vote in votes {
#expect(!vote.option.isEmpty)
}
}
@Test("fetchPollVotes total vote count matches individual counts")
func voteTotals() async throws {
let votes = try await client.fetchPollVotes(for: livePollURL, from: relay)
let total = votes.reduce(0) { $0 + $1.voteCount }
#expect(total >= 0)
}
}
@Suite("RelayClient groups", .serialized)
struct GroupsTests {
let client = RelayClient()
@Test("fetchGroupList returns known groups")
func groupListNonEmpty() async throws {
let groups = try await client.fetchGroupList(from: relay)
#expect(!groups.isEmpty)
}
@Test("group names and slugs are non-empty strings")
func groupNamesValid() async throws {
let groups = try await client.fetchGroupList(from: relay)
for g in groups {
#expect(!g.name.isEmpty)
#expect(!g.slug.isEmpty)
}
}
@Test("fetchGroupPostURLs returns posts for Emacs group")
func emacsGroupPosts() async throws {
let urls = try await client.fetchGroupPostURLs(slug: "emacs", from: relay)
#expect(!urls.isEmpty)
for url in urls {
#expect(url.contains("#"), "Group post URL missing #: \(url)")
}
}
}