Files
andros d26b9ab3b6 Add boost, poll, migration, pinned posts, edit profile, follow/unfollow, groups, visibility badge, mood, scheduled posts
- 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
2026-04-19 18:46:01 +02:00

143 lines
5.2 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 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))
}
}