Files
ghostgram/submodules/TextFormat/Tests/ReplyMarkupButtonActionTests.swift
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

267 lines
13 KiB
Swift

import XCTest
import Postbox
import TelegramApi
import TelegramCore
/// Covers the two shared `Api` → `ReplyMarkupButtonAction` mappers introduced when the schema
/// collapsed 16 per-behaviour keyboard-button constructors into `keyboardButton` (ButtonType) and
/// `keyboardInlineButton` (InlineButtonType). The schema has since moved `keyboardInlineButton` into
/// its own `KeyboardInlineButton` type (with a matching `keyboardInlineButtonRow`), but the two
/// action mappers below are unaffected — `Api.ButtonType` and `Api.InlineButtonType` did not change.
///
/// Scope limit worth knowing: `ReplyMarkupButton(apiButton:)` and
/// `ReplyMarkupMessageAttribute(apiMarkup:)` are **internal** to TelegramCore, so the button-level
/// assembly (title + style + fwdText threading) is not reachable from this test module and is not
/// covered here. Only the `public` action mappers are. Widening TelegramCore's API surface purely
/// for a test was judged the wrong trade.
final class ReplyMarkupButtonActionTests: XCTestCase {
private func postboxRoundTrip(_ action: ReplyMarkupButtonAction) -> ReplyMarkupButtonAction {
let encoder = PostboxEncoder()
action.encode(encoder)
return ReplyMarkupButtonAction(decoder: PostboxDecoder(buffer: MemoryBuffer(data: encoder.makeData())))
}
private func inlineAction(_ type: Api.InlineButtonType) -> ReplyMarkupButtonAction {
return ReplyMarkupButtonAction.from(apiType: type).action
}
private func keyboardAction(_ type: Api.ButtonType) -> ReplyMarkupButtonAction {
return ReplyMarkupButtonAction.from(apiType: type).action
}
// MARK: - .disabled Postbox coding (discriminator 14)
func test_disabled_postboxRoundTrip() {
XCTAssertEqual(self.postboxRoundTrip(.disabled), .disabled)
}
/// Discriminator 14 must not collide with an existing tag. If it did, a stored `.disabled`
/// would decode as something tappable — the exact failure this case exists to prevent.
func test_disabled_doesNotCollideWithOtherTags() {
XCTAssertNotEqual(self.postboxRoundTrip(.disabled), .text)
XCTAssertNotEqual(self.postboxRoundTrip(.disabled), .payment)
XCTAssertNotEqual(self.postboxRoundTrip(.disabled), .openWebApp)
XCTAssertNotEqual(self.postboxRoundTrip(.disabled), .copyText(payload: ""))
}
/// Every pre-existing action must still survive its own round-trip — the new tag must not have
/// disturbed the established numbering.
func test_existingActions_stillRoundTrip() {
XCTAssertEqual(self.postboxRoundTrip(.text), .text)
XCTAssertEqual(self.postboxRoundTrip(.url("https://t.me")), .url("https://t.me"))
XCTAssertEqual(self.postboxRoundTrip(.requestPhone), .requestPhone)
XCTAssertEqual(self.postboxRoundTrip(.requestMap), .requestMap)
XCTAssertEqual(self.postboxRoundTrip(.openWebApp), .openWebApp)
XCTAssertEqual(self.postboxRoundTrip(.payment), .payment)
XCTAssertEqual(self.postboxRoundTrip(.urlAuth(url: "https://a", buttonId: 7)),
.urlAuth(url: "https://a", buttonId: 7))
XCTAssertEqual(self.postboxRoundTrip(.setupPoll(isQuiz: true)), .setupPoll(isQuiz: true))
XCTAssertEqual(self.postboxRoundTrip(.setupPoll(isQuiz: nil)), .setupPoll(isQuiz: nil))
XCTAssertEqual(self.postboxRoundTrip(.openWebView(url: "https://w", simple: true)),
.openWebView(url: "https://w", simple: true))
XCTAssertEqual(self.postboxRoundTrip(.copyText(payload: "abc")), .copyText(payload: "abc"))
}
// MARK: - Api.InlineButtonType mapping (all 12 constructors)
func test_inlineButtonType_url() {
XCTAssertEqual(self.inlineAction(.inlineButtonTypeUrl(.init(url: "https://t.me"))), .url("https://t.me"))
}
func test_inlineButtonType_webView() {
XCTAssertEqual(self.inlineAction(.inlineButtonTypeWebView(.init(url: "https://w"))),
.openWebView(url: "https://w", simple: false))
}
func test_inlineButtonType_copy() {
XCTAssertEqual(self.inlineAction(.inlineButtonTypeCopy(.init(copyText: "payload"))),
.copyText(payload: "payload"))
}
func test_inlineButtonType_game_and_buy() {
XCTAssertEqual(self.inlineAction(.inlineButtonTypeGame), .openWebApp)
XCTAssertEqual(self.inlineAction(.inlineButtonTypeBuy), .payment)
}
func test_inlineButtonType_disabled() {
XCTAssertEqual(self.inlineAction(.inlineButtonTypeDisabled), .disabled)
}
func test_inlineButtonType_userProfile() {
let expected = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(42))
XCTAssertEqual(self.inlineAction(.inlineButtonTypeUserProfile(.init(userId: 42))),
.openUserProfile(peerId: expected))
}
/// The input variant carries an InputUser the client cannot resolve, so it degenerates to id 0.
/// Preserved from the pre-unification behaviour deliberately.
func test_inlineButtonType_inputUserProfile_degeneratesToZero() {
let zero = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0))
XCTAssertEqual(self.inlineAction(.inputInlineButtonTypeUserProfile(.init(userId: .inputUserEmpty))),
.openUserProfile(peerId: zero))
}
func test_inlineButtonType_callback_carriesDataAndPasswordFlag() {
guard case let .callback(requiresPassword, data) = self.inlineAction(
.inlineButtonTypeCallback(.init(flags: 1 << 0, data: Buffer(data: Data([1, 2, 3]))))
) else {
return XCTFail("expected callback")
}
XCTAssertTrue(requiresPassword)
XCTAssertEqual(data.makeData(), Data([1, 2, 3]))
}
func test_inlineButtonType_callback_withoutPasswordFlag() {
guard case let .callback(requiresPassword, _) = self.inlineAction(
.inlineButtonTypeCallback(.init(flags: 0, data: Buffer(data: Data())))
) else {
return XCTFail("expected callback")
}
XCTAssertFalse(requiresPassword)
}
func test_inlineButtonType_switchInline_samePeerAndPeerTypes() {
guard case let .switchInline(samePeer, query, peerTypes) = self.inlineAction(
.inlineButtonTypeSwitchInline(.init(flags: 1 << 0, query: "q", peerTypes: [.inlineQueryPeerTypeBotPM]))
) else {
return XCTFail("expected switchInline")
}
XCTAssertTrue(samePeer)
XCTAssertEqual(query, "q")
XCTAssertTrue(peerTypes.contains(.bots))
}
func test_inlineButtonType_switchInline_nilPeerTypesIsEmpty() {
guard case let .switchInline(samePeer, _, peerTypes) = self.inlineAction(
.inlineButtonTypeSwitchInline(.init(flags: 0, query: "", peerTypes: nil))
) else {
return XCTFail("expected switchInline")
}
XCTAssertFalse(samePeer)
XCTAssertTrue(peerTypes.isEmpty)
}
// MARK: - fwd_text, which now lives only on the urlAuth constructors
func test_urlAuth_carriesFwdText() {
let result = ReplyMarkupButtonAction.from(apiType: .inlineButtonTypeUrlAuth(
.init(flags: 1 << 0, fwdText: "Forwarded label", url: "https://a", buttonId: 7)))
XCTAssertEqual(result.action, .urlAuth(url: "https://a", buttonId: 7))
XCTAssertEqual(result.fwdText, "Forwarded label")
}
func test_urlAuth_absentFwdTextIsNil() {
let result = ReplyMarkupButtonAction.from(apiType: .inlineButtonTypeUrlAuth(
.init(flags: 0, fwdText: nil, url: "https://a", buttonId: 3)))
XCTAssertEqual(result.action, .urlAuth(url: "https://a", buttonId: 3))
XCTAssertNil(result.fwdText)
}
/// The input variant has no buttonId on the wire; 0 is the established stand-in.
func test_inputUrlAuth_buttonIdIsZeroAndFwdTextSurvives() {
let result = ReplyMarkupButtonAction.from(apiType: .inputInlineButtonTypeUrlAuth(
.init(flags: 0, fwdText: "Label", url: "https://a", bot: .inputUserEmpty)))
XCTAssertEqual(result.action, .urlAuth(url: "https://a", buttonId: 0))
XCTAssertEqual(result.fwdText, "Label")
}
/// No non-urlAuth inline constructor may invent a fwdText.
func test_nonUrlAuthInlineTypes_haveNoFwdText() {
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .inlineButtonTypeUrl(.init(url: "u"))).fwdText)
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .inlineButtonTypeDisabled).fwdText)
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .inlineButtonTypeGame).fwdText)
}
// MARK: - Api.ButtonType mapping (all 7 constructors)
/// The old plain `keyboardButton` is now `keyboardButton type=buttonTypeDefault`.
func test_buttonTypeDefault_isPlainText() {
XCTAssertEqual(self.keyboardAction(.buttonTypeDefault), .text)
}
func test_buttonType_phoneAndLocation() {
XCTAssertEqual(self.keyboardAction(.buttonTypeRequestPhone), .requestPhone)
XCTAssertEqual(self.keyboardAction(.buttonTypeRequestGeoLocation), .requestMap)
}
/// `buttonTypeSimpleWebView` is the only ButtonType producing openWebView, and it is always
/// simple: true — the distinction from the inline webView constructor.
func test_buttonType_simpleWebView_isSimple() {
XCTAssertEqual(self.keyboardAction(.buttonTypeSimpleWebView(.init(url: "https://s"))),
.openWebView(url: "https://s", simple: true))
}
func test_buttonType_requestPoll_quizFlagVariants() {
XCTAssertEqual(self.keyboardAction(.buttonTypeRequestPoll(.init(flags: 1 << 0, quiz: .boolTrue))),
.setupPoll(isQuiz: true))
XCTAssertEqual(self.keyboardAction(.buttonTypeRequestPoll(.init(flags: 1 << 0, quiz: .boolFalse))),
.setupPoll(isQuiz: false))
XCTAssertEqual(self.keyboardAction(.buttonTypeRequestPoll(.init(flags: 0, quiz: nil))),
.setupPoll(isQuiz: nil))
}
func test_buttonType_requestPeer_user() {
guard case let .requestPeer(peerType, buttonId, maxQuantity) = self.keyboardAction(
.buttonTypeRequestPeer(.init(
flags: 0,
buttonId: 5,
peerType: .requestPeerTypeUser(.init(flags: 0, bot: .boolTrue, premium: nil)),
maxQuantity: 3))
) else {
return XCTFail("expected requestPeer")
}
XCTAssertEqual(buttonId, 5)
XCTAssertEqual(maxQuantity, 3)
guard case let .user(user) = peerType else {
return XCTFail("expected user peer type")
}
XCTAssertEqual(user.isBot, true)
XCTAssertNil(user.isPremium)
}
/// The input variant must map identically — the two arms used to be duplicated verbatim and are
/// now one initializer, so this guards the de-duplication.
func test_inputButtonType_requestPeer_matchesNonInputVariant() {
let peerType = Api.RequestPeerType.requestPeerTypeBroadcast(.init(
flags: 1 << 0, hasUsername: .boolTrue, userAdminRights: nil, botAdminRights: nil))
let plain = self.keyboardAction(.buttonTypeRequestPeer(.init(
flags: 0, buttonId: 9, peerType: peerType, maxQuantity: 1)))
let input = self.keyboardAction(.inputButtonTypeRequestPeer(.init(
flags: 0, buttonId: 9, peerType: peerType, maxQuantity: 1)))
XCTAssertEqual(plain, input)
}
func test_buttonType_requestPeer_groupFlags() {
guard case let .requestPeer(peerType, _, _) = self.keyboardAction(
.buttonTypeRequestPeer(.init(
flags: 0,
buttonId: 1,
peerType: .requestPeerTypeChat(.init(
flags: (1 << 0) | (1 << 5),
hasUsername: nil,
forum: .boolTrue,
userAdminRights: nil,
botAdminRights: nil)),
maxQuantity: 1))
) else {
return XCTFail("expected requestPeer")
}
guard case let .group(group) = peerType else {
return XCTFail("expected group peer type")
}
XCTAssertTrue(group.isCreator)
XCTAssertTrue(group.botParticipant)
XCTAssertEqual(group.isForum, true)
XCTAssertNil(group.hasUsername)
}
/// No ButtonType constructor may produce fwdText — fwd_text is inline-only in the new schema.
func test_buttonTypes_neverProduceFwdText() {
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .buttonTypeDefault).fwdText)
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .buttonTypeRequestPhone).fwdText)
XCTAssertNil(ReplyMarkupButtonAction.from(apiType: .buttonTypeSimpleWebView(.init(url: "u"))).fwdText)
}
}