d26b9ab3b6
- Library: NewPostOptions factories for boost/poll/vote/migration/reaction; PostWriter emits new properties; ProfileWriter for header manipulation; OrgSocialPollVote model; RelayClient.fetchPollVotes/fetchGroupList/fetchGroupPostURLs - Tests: ProfileWriterTests (14), SpecialPostTests (17) — 117 total, all passing - App: ComposeView/ViewModel support poll options, poll end date, scheduled posts; PostRowView shows boost confirm, poll with vote bars, reaction/migration/visibility badges, mood; BoostViewModel, EditProfileViewModel with async load, GroupsViewModel; EditProfileView, GroupsView with group post list; ProfileView toolbar with follow/unfollow and edit button, pinned post section; full uploader support across all views; Groups tab in RootView
143 lines
5.2 KiB
Swift
143 lines
5.2 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import OrgSocialKit
|
||
|
||
private let sampleProfile = """
|
||
#+TITLE: My Feed
|
||
#+NICK: andros
|
||
#+DESCRIPTION: Developer.
|
||
#+FOLLOW: shom https://shom.dev/social.org
|
||
#+FOLLOW: https://tanrax.com/social.org
|
||
|
||
* Posts
|
||
|
||
** 2025-01-01T10:00:00+01:00
|
||
:PROPERTIES:
|
||
:CLIENT: org-social-ios
|
||
:END:
|
||
|
||
Hello!
|
||
"""
|
||
|
||
@Suite("ProfileWriter – keyword editing")
|
||
struct ProfileWriterKeywordTests {
|
||
|
||
let pw = ProfileWriter()
|
||
|
||
@Test("setKeyword replaces existing value")
|
||
func replaceExisting() {
|
||
let result = pw.setKeyword("NICK", value: "newNick", in: sampleProfile)
|
||
#expect(result.contains("#+NICK: newNick"))
|
||
#expect(!result.contains("#+NICK: andros"))
|
||
}
|
||
|
||
@Test("setKeyword inserts new keyword before * Posts")
|
||
func insertBeforePosts() {
|
||
let result = pw.setKeyword("AVATAR", value: "https://example.com/avatar.jpg", in: sampleProfile)
|
||
#expect(result.contains("#+AVATAR: https://example.com/avatar.jpg"))
|
||
let avatarIdx = result.range(of: "#+AVATAR:")!.lowerBound
|
||
let postsIdx = result.range(of: "* Posts")!.lowerBound
|
||
#expect(avatarIdx < postsIdx)
|
||
}
|
||
|
||
@Test("setKeyword is case-insensitive on keyword")
|
||
func caseInsensitive() {
|
||
let result = pw.setKeyword("description", value: "New bio", in: sampleProfile)
|
||
#expect(result.contains("#+DESCRIPTION: New bio"))
|
||
#expect(!result.contains("#+DESCRIPTION: Developer."))
|
||
}
|
||
|
||
@Test("removeKeyword deletes the line")
|
||
func removeKeyword() {
|
||
let result = pw.removeKeyword("DESCRIPTION", from: sampleProfile)
|
||
#expect(!result.contains("#+DESCRIPTION:"))
|
||
#expect(result.contains("#+NICK: andros"))
|
||
}
|
||
|
||
@Test("removeKeyword is a no-op when keyword absent")
|
||
func removeAbsent() {
|
||
let result = pw.removeKeyword("AVATAR", from: sampleProfile)
|
||
#expect(result == sampleProfile)
|
||
}
|
||
|
||
@Test("setMultiKeyword replaces all LINK lines")
|
||
func setMultiLink() {
|
||
let withLinks = sampleProfile + "\n#+LINK: https://old.example.com"
|
||
let result = pw.setMultiKeyword("LINK", values: ["https://a.com", "https://b.com"], in: withLinks)
|
||
#expect(result.contains("#+LINK: https://a.com"))
|
||
#expect(result.contains("#+LINK: https://b.com"))
|
||
#expect(!result.contains("https://old.example.com"))
|
||
}
|
||
|
||
@Test("setMultiKeyword with empty values removes all lines")
|
||
func removeAllLinks() {
|
||
let withLinks = sampleProfile + "\n#+LINK: https://old.example.com"
|
||
let result = pw.setMultiKeyword("LINK", values: [], in: withLinks)
|
||
#expect(!result.contains("#+LINK:"))
|
||
}
|
||
}
|
||
|
||
@Suite("ProfileWriter – follow management")
|
||
struct ProfileWriterFollowTests {
|
||
|
||
let pw = ProfileWriter()
|
||
let newURL = URL(string: "https://friend.example.com/social.org")!
|
||
|
||
@Test("addFollow with nick inserts formatted line")
|
||
func addFollowWithNick() {
|
||
let result = pw.addFollow(url: newURL, nick: "friend", to: sampleProfile)
|
||
#expect(result.contains("#+FOLLOW: friend https://friend.example.com/social.org"))
|
||
}
|
||
|
||
@Test("addFollow without nick inserts URL-only line")
|
||
func addFollowWithoutNick() {
|
||
let result = pw.addFollow(url: newURL, to: sampleProfile)
|
||
#expect(result.contains("#+FOLLOW: https://friend.example.com/social.org"))
|
||
}
|
||
|
||
@Test("addFollow inserts after last existing FOLLOW")
|
||
func addFollowAfterExisting() {
|
||
let result = pw.addFollow(url: newURL, nick: "friend", to: sampleProfile)
|
||
let lines = result.components(separatedBy: "\n")
|
||
let followLines = lines.enumerated().filter { $0.element.uppercased().hasPrefix("#+FOLLOW:") }
|
||
let newIdx = followLines.last { $0.element.contains("friend") }?.offset ?? -1
|
||
let oldIdx = followLines.first { $0.element.contains("tanrax") }?.offset ?? -1
|
||
#expect(newIdx > oldIdx)
|
||
}
|
||
|
||
@Test("addFollow is idempotent when already following")
|
||
func addFollowIdempotent() {
|
||
let existingURL = URL(string: "https://shom.dev/social.org")!
|
||
let result = pw.addFollow(url: existingURL, nick: "shom", to: sampleProfile)
|
||
let count = result.components(separatedBy: "\n")
|
||
.filter { $0.contains("shom.dev") }.count
|
||
#expect(count == 1)
|
||
}
|
||
|
||
@Test("removeFollow removes matching line")
|
||
func removeFollow() {
|
||
let url = URL(string: "https://shom.dev/social.org")!
|
||
let result = pw.removeFollow(url: url, from: sampleProfile)
|
||
#expect(!result.contains("shom.dev"))
|
||
#expect(result.contains("tanrax.com"))
|
||
}
|
||
|
||
@Test("removeFollow leaves other follows intact")
|
||
func removeFollowLeavesOthers() {
|
||
let url = URL(string: "https://shom.dev/social.org")!
|
||
let result = pw.removeFollow(url: url, from: sampleProfile)
|
||
#expect(result.contains("#+FOLLOW: https://tanrax.com/social.org"))
|
||
}
|
||
|
||
@Test("isFollowing returns true for followed URL")
|
||
func isFollowingTrue() {
|
||
let url = URL(string: "https://shom.dev/social.org")!
|
||
#expect(pw.isFollowing(url: url, in: sampleProfile))
|
||
}
|
||
|
||
@Test("isFollowing returns false for unknown URL")
|
||
func isFollowingFalse() {
|
||
#expect(!pw.isFollowing(url: newURL, in: sampleProfile))
|
||
}
|
||
}
|