Signal-iOS/SignalServiceKit/tests/Messages/TSInfoMessage+GroupUpdateCopy+NSAttributedStringTest.swift
Evan Hahn 370ff654e7
Change license to AGPL
Change license to AGPL

This commit:

- Updates the `LICENSE` file

- Start every file with something like:

      // Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
      // SPDX-License-Identifier: AGPL-3.0-only

---

First, I removed existing license headers with this Ruby 3.1.2 script:

    require 'set'

    EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']

    same = 0
    different = 0

    all_files = `git ls-files`.lines.map { |line| line.strip }
    all_files.each do |relative_path|
      if relative_path == 'Pods'
        next
      end

      unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
        next
      end

      path = File.expand_path(relative_path)

      contents = File.read(path)
      new_contents = contents.sub(/\/\/\n\/\/  Copyright .*\n\/\/\n\n/, '')

      if contents == new_contents
        same += 1
      else
        different += 1
      end

      File.write(path, new_contents)
    end

    puts "updated #{different} file(s), left #{same} untouched"

I'm sure this script could be improved, but it worked well enough.

Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.

Then I fixed some stragglers and updated the precommit script.

See [a similar change in the Desktop app][0].

[0]: 8bfaf598af
2022-10-13 08:25:37 -05:00

383 lines
16 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
@testable import SignalServiceKit
import XCTest
class NSAttributedStringInGroupUpdateCopyTest: SSKBaseTestSwift {
private func assertEqual(_ one: NSAttributedString, _ two: NSAttributedString) {
XCTAssertEqual(one, two)
}
func test_XMadeYAnAdmin() {
assertEqual(.aliceMadeBobAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .bob))
assertEqual(.aliceHMadeAliceAdmin, NSAttributedString.make(firstUser: .aliceH, madeAdmin: .alice))
assertEqual(.aliceMadeMadeAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .made))
assertEqual(.aliceMadeAnotherAliceAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .anotherAlice))
assertEqual(.aliceMadeSingleArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .singleArgFormatSpecifier))
assertEqual(.aliceMadeMultiArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .multiArgFormatSpecifier))
assertEqual(.aliceMadeEmptyAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .empty))
}
func test_XMadeYAnAdmin_ArabicNameInEnglishString() {
assertEqual(.aliceMadeFatimaInArabicAnAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .fatima))
}
func test_XMadeYAnAdmin_EnglishNameInArabicString() {
assertEqual(.aliceMadeBobAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .alice, madeAdmin: .bob))
}
func test_XMadeYAnAdmin_ChineseNameInEnglishString() {
assertEqual(.qiInChineseMadeAliceAdmin, NSAttributedString.make(firstUser: .qi, madeAdmin: .alice))
}
func test_XMadeYAnAdmin_ChineseAndArabicNamesInArabicString() {
assertEqual(.qiInChineseMadeFatimaInArabicAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .qi, madeAdmin: .fatima))
}
/// Note that this test involves formatting numbers into a format string,
/// which is done in the guts of Signal using the "current locale".
/// Therefore, running this test outside of the "United States" locale in
/// English will fail due to substitution errors.
func test_XInvitedNPeople() throws {
try XCTSkipIf(Locale.current.identifier != "en_US", "This test requires the en_US locale for number formatting.")
assertEqual(.aliceInvitedOnePerson, NSAttributedString.make(user: .alice, invitedNPeople: 1))
assertEqual(.aliceInvitedTwoPeople, NSAttributedString.make(user: .alice, invitedNPeople: 2))
assertEqual(.aliceInvitedOneMillionPeople, NSAttributedString.make(user: .alice, invitedNPeople: 1000000))
assertEqual(.emptyInvitedThreePeople, NSAttributedString.make(user: .empty, invitedNPeople: 3))
}
/// Technically this tests a string that is never used in group updates.
/// However, we don't have any relevant strings that have the name at the
/// end of the string, and I wanted to test that.
func test_NameAtEndOfString() {
assertEqual(
.aliceSomethingWithBobsNameAtEndOfString,
NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "en")
)
assertEqual(
.aliceSomethingWithBobsNameAtEndOfString_Arabic,
NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "ar")
)
}
/// Some localized strings invert the "expected order" of format args, e.g.
/// `%2$@` may be placed before `%1$@` for a given language simply based on
/// the grammar of that language. This tests for that, using a string that
/// is known to have this behavior in Malayalam.
func test_LocalizedStringInvertsFormatArgOrder() {
assertEqual(
.aliceSomethingBobWithInvertedNameOrder,
NSAttributedString.makeWithInvertedFormatArgOrder(firstUser: .alice, secondUser: .bob)
)
}
}
// MARK: - "X made Y an admin"
private extension NSAttributedString {
static let aliceMadeBobAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .bob) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.bob, toRange: NSRange(location: 13, length: 5))
return mutable
}()
static let aliceHMadeAliceAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .aliceH) made \(user: .alice) an admin.")
mutable.addAddress(.aliceH, toRange: NSRange(location: 0, length: 9))
mutable.addAddress(.alice, toRange: NSRange(location: 15, length: 7))
return mutable
}()
static let aliceMadeMadeAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .made) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.made, toRange: NSRange(location: 13, length: 6))
return mutable
}()
static let aliceMadeAnotherAliceAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .anotherAlice) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.anotherAlice, toRange: NSRange(location: 13, length: 7))
return mutable
}()
static let aliceMadeSingleArgFormatSpecifierAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .singleArgFormatSpecifier) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.singleArgFormatSpecifier, toRange: NSRange(location: 13, length: 4))
return mutable
}()
static let aliceMadeMultiArgFormatSpecifierAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .multiArgFormatSpecifier) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.multiArgFormatSpecifier, toRange: NSRange(location: 13, length: 6))
return mutable
}()
/// This should theoretically be impossible - we should never in practice
/// have an empty substitution. However, if we do, we don't want to fail
/// here :)
static let aliceMadeEmptyAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .empty) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.empty, toRange: NSRange(location: 13, length: 2))
return mutable
}()
static func make(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
make(
fromFormat: .xMadeYAnAdminFormat,
groupUpdateFormatArgs: [
.name(firstUser.name, firstUser.address),
.name(secondUser.name, secondUser.address)
]
)
}
}
// MARK: - "X made Y an admin", but using non-English languages
private extension NSAttributedString {
static let aliceMadeBobAdmin_Arabic: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "إنَّ \(user: .alice) قد جعلَ \(user: .bob) مُشرفاً.")
mutable.addAddress(.alice, toRange: NSRange(location: 5, length: 7))
mutable.addAddress(.bob, toRange: NSRange(location: 21, length: 5))
return mutable
}()
static let aliceMadeFatimaInArabicAnAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .fatima) an admin.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.fatima, toRange: NSRange(location: 13, length: 10))
return mutable
}()
static let qiInChineseMadeAliceAdmin: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .qi) made \(user: .alice) an admin.")
mutable.addAddress(.qi, toRange: NSRange(location: 0, length: 3))
mutable.addAddress(.alice, toRange: NSRange(location: 9, length: 7))
return mutable
}()
static let qiInChineseMadeFatimaInArabicAdmin_Arabic: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "إنَّ \(user: .qi) قد جعلَ \(user: .fatima) مُشرفاً.")
mutable.addAddress(.qi, toRange: NSRange(location: 5, length: 3))
mutable.addAddress(.fatima, toRange: NSRange(location: 17, length: 10))
return mutable
}()
static func makeArabic(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
make(
fromFormat: .xMadeYAnAdminFormat_Arabic,
groupUpdateFormatArgs: [
.name(firstUser.name, firstUser.address),
.name(secondUser.name, secondUser.address)
]
)
}
}
// MARK: - "X invited N people"
private extension NSAttributedString {
static let aliceInvitedOnePerson: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1 person to the group.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
return mutable
}()
static let aliceInvitedTwoPeople: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 2 people to the group.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
return mutable
}()
static let aliceInvitedOneMillionPeople: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1,000,000 people to the group.")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
return mutable
}()
static let emptyInvitedThreePeople: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .empty) invited 3 people to the group.")
mutable.addAddress(.empty, toRange: NSRange(location: 0, length: 2))
return mutable
}()
static func make(user: ReferencedUser, invitedNPeople: Int) -> NSAttributedString {
make(
fromFormat: .xInvitedNPeopleFormat,
groupUpdateFormatArgs: [.raw(invitedNPeople), .name(user.name, user.address)]
)
}
}
// MARK: - Name at end of string
private extension NSAttributedString {
static let aliceSomethingWithBobsNameAtEndOfString: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) to \(user: .bob)")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.bob, toRange: NSRange(location: 11, length: 5))
return mutable
}()
static let aliceSomethingWithBobsNameAtEndOfString_Arabic: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .alice) إلى \(user: .bob)")
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
mutable.addAddress(.bob, toRange: NSRange(location: 12, length: 5))
return mutable
}()
static func makeWithNameAtEndOfString(
firstUser: ReferencedUser,
userAtEndOfString otherUser: ReferencedUser,
lang: String
) -> NSAttributedString {
make(
fromFormat: String.newGroupMessageNotificationTitleFormat(lang: lang),
groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(otherUser.name, otherUser.address)]
)
}
}
// MARK: - Localized string inverts format arg order
private extension NSAttributedString {
static let aliceSomethingBobWithInvertedNameOrder: NSAttributedString = {
let mutable = NSMutableAttributedString(string: "\(user: .bob)”-এ “\(user: .alice)” যোগ করবেন?")
mutable.addAddress(.bob, toRange: NSRange(location: 1, length: 5))
mutable.addAddress(.alice, toRange: NSRange(location: 11, length: 7))
return mutable
}()
/// The returned string formats the "second" user passed before the "first"
/// user, simply as a result of the format string.
static func makeWithInvertedFormatArgOrder(firstUser: ReferencedUser, secondUser: ReferencedUser) -> NSAttributedString {
make(
fromFormat: .addToGroupActionSheetMessage_Bangla,
groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(secondUser.name, secondUser.address)]
)
}
}
// MARK: - Add addresses
private extension NSMutableAttributedString {
func addAddress(_ address: SignalServiceAddress, toRange range: NSRange) {
addAttribute(.addressOfName, value: address, range: range)
}
}
// MARK: - Users
private extension SignalServiceAddress {
static let alice = SignalServiceAddress(phoneNumber: "+17735550100")
static let aliceH = SignalServiceAddress(phoneNumber: "+17735550101")
static let bob = SignalServiceAddress(phoneNumber: "+17735550102")
static let made = SignalServiceAddress(phoneNumber: "+17735550103")
static let anotherAlice = SignalServiceAddress(phoneNumber: "+17735550104")
static let singleArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550105")
static let multiArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550106")
static let empty = SignalServiceAddress(phoneNumber: "+17735550107")
static let fatima = SignalServiceAddress(phoneNumber: "+17735550108")
static let qi = SignalServiceAddress(phoneNumber: "+17735550109")
}
private struct ReferencedUser {
static let alice: ReferencedUser = ReferencedUser(name: "Alice", address: .alice)
static let aliceH: ReferencedUser = ReferencedUser(name: "Alice H", address: .aliceH)
static let bob: ReferencedUser = ReferencedUser(name: "Bob", address: .bob)
static let made: ReferencedUser = ReferencedUser(name: "made", address: .made)
static let anotherAlice: ReferencedUser = ReferencedUser(name: "Alice", address: .anotherAlice)
static let singleArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%@", address: .singleArgFormatSpecifier)
static let multiArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%1$@", address: .multiArgFormatSpecifier)
static let empty: ReferencedUser = ReferencedUser(name: "", address: .empty)
static let fatima: ReferencedUser = ReferencedUser(name: "فَاطِمَة", address: .fatima)
static let qi: ReferencedUser = ReferencedUser(name: "", address: .qi)
let name: String
let address: SignalServiceAddress
}
private extension String.StringInterpolation {
/// We wrap all formatted names in Unicode isolates.
mutating func appendInterpolation(user: ReferencedUser) {
appendLiteral("\u{2068}\(user.name)\u{2069}")
}
}
// MARK: - Localized format strings
/// We should ideally use ``OWSLocalizedString`` for this, but it doesn't
/// support specifying the locale (uses the current).
private extension String {
static let xMadeYAnAdminFormat: String = {
try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT")
}()
static let xMadeYAnAdminFormat_Arabic: String = {
try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", lang: "ar")
}()
static let xInvitedNPeopleFormat: String = {
try! localized(key: "GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_%d", tableName: "PluralAware")
}()
static func newGroupMessageNotificationTitleFormat(lang: String) -> String {
try! localized(key: "NEW_GROUP_MESSAGE_NOTIFICATION_TITLE", lang: lang)
}
static let addToGroupActionSheetMessage_Bangla: String = {
try! localized(key: "ADD_TO_GROUP_ACTION_SHEET_MESSAGE_FORMAT", lang: "bn")
}()
static func localized(
key: String,
tableName: String? = nil,
lang: String = "en"
) throws -> String {
guard
let bundlePath = Bundle.main.path(forResource: lang, ofType: "lproj"),
let bundle = Bundle(path: bundlePath)
else {
throw OWSAssertionError("Failed to find bundle for lang code \(lang)")
}
return NSLocalizedString(
key,
tableName: tableName,
bundle: bundle,
value: "",
comment: ""
)
}
}