keytrans: Add reset account data field function
Some checks failed
[CI] Check Versions / Check version number consistency (push) Has been cancelled
Some checks failed
[CI] Check Versions / Check version number consistency (push) Has been cancelled
This commit is contained in:
parent
c41e917d4e
commit
7c8cb0c5fc
@ -1,3 +1,5 @@
|
||||
v0.94.1
|
||||
|
||||
- Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads.
|
||||
|
||||
- keytrans: Add reset account data field functionality for all platforms.
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
//
|
||||
package org.signal.libsignal.net
|
||||
|
||||
public abstract class KeyTransparency {
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.keytrans.Store
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public object KeyTransparency {
|
||||
/**
|
||||
* Mode of the key transparency operation.
|
||||
*
|
||||
@ -32,4 +36,44 @@ public abstract class KeyTransparency {
|
||||
|
||||
public fun isSelf(): Boolean = this is Self
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag identifying an optional field of the account data.
|
||||
*
|
||||
* (Must be in sync with the Rust counterpart)
|
||||
*/
|
||||
public enum class AccountDataField(
|
||||
public val value: Int,
|
||||
) {
|
||||
E164(0),
|
||||
USERNAME_HASH(1),
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a particular field in the data associated with given ACI.
|
||||
*
|
||||
* Must only be called for the "self" account when either E.164 or username change is performed.
|
||||
*
|
||||
* Upon successful completion the data associated with the account will be updated in the store, if it
|
||||
* was present to begin with, noop if it was not.
|
||||
*
|
||||
* @param aci An ACI of "self" account.
|
||||
* @param field Account data field to be reset (E.164 or username hash)
|
||||
* @param store local persistent storage for key transparency-related data.
|
||||
* @throws IllegalArgumentException if the stored data cannot be decoded correctly, which means data corruption.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun resetField(
|
||||
aci: ServiceId.Aci,
|
||||
field: AccountDataField,
|
||||
store: Store,
|
||||
) {
|
||||
store.getAccountData(aci).map {
|
||||
val updated = Native.KeyTransparency_ResetDataField(it, field.value)
|
||||
if (updated.isEmpty()) {
|
||||
throw IllegalArgumentException("failed to decode account data")
|
||||
}
|
||||
store.setAccountData(aci, updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import java.util.UUID;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.keytrans.KeyTransparencyException;
|
||||
import org.signal.libsignal.keytrans.TestStore;
|
||||
import org.signal.libsignal.keytrans.VerificationFailedException;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
@ -60,4 +61,29 @@ public class KeyTransparencyTest {
|
||||
public void canBridgeChatSendError() {
|
||||
assertThrows(TimeoutException.class, NativeTesting::TESTING_KeyTransChatSendError);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldThrowsOnCorruptData() {
|
||||
var store = new TestStore();
|
||||
store.setAccountData(TEST_ACI, new byte[] {1, 2, 3});
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldIsNoopWhenDataIsMissing() {
|
||||
var store = new TestStore();
|
||||
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
|
||||
assert (store.storage.get(TEST_ACI).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldUpdatesStoreOnSuccess() {
|
||||
var store = new TestStore();
|
||||
store.setAccountData(TEST_ACI, NativeTesting.TESTING_KeyTransStoredAccountData());
|
||||
assertEquals(1, store.storage.get(TEST_ACI).size());
|
||||
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
|
||||
assertEquals(2, store.storage.get(TEST_ACI).size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,6 +629,8 @@ internal object Native {
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_E164SearchKey(e164: String): ByteArray
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_ResetDataField(accountData: ByteArray, field: Int): ByteArray
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_UsernameHashSearchKey(hash: ByteArray): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@ -186,6 +186,8 @@ public object NativeTesting {
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun TESTING_KeyTransNonFatalVerificationFailure(): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_KeyTransStoredAccountData(): ByteArray
|
||||
@JvmStatic
|
||||
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_Destroy(handle: ObjectHandle): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_New(): ObjectHandle
|
||||
|
||||
@ -318,6 +318,7 @@ type NativeFunctions = {
|
||||
KeyTransparency_AciSearchKey: (aci: Uint8Array<ArrayBuffer>,) => Uint8Array<ArrayBuffer>;
|
||||
KeyTransparency_Check: (asyncRuntime: Wrapper<TokioAsyncContext>,environment: number,chatConnection: Wrapper<UnauthenticatedChatConnection>,aci: Uint8Array<ArrayBuffer>,aciIdentityKey: Wrapper<PublicKey>,e164: (string | null),unidentifiedAccessKey: (Uint8Array<ArrayBuffer> | null),usernameHash: (Uint8Array<ArrayBuffer> | null),accountData: (Uint8Array<ArrayBuffer> | null),lastDistinguishedTreeHead: (Uint8Array<ArrayBuffer> | null),isSelfCheck: boolean,isE164Discoverable: boolean,) => CancellablePromise<[Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>]>;
|
||||
KeyTransparency_E164SearchKey: (e164: string,) => Uint8Array<ArrayBuffer>;
|
||||
KeyTransparency_ResetDataField: (accountData: Uint8Array<ArrayBuffer>,field: number,) => Uint8Array<ArrayBuffer>;
|
||||
KeyTransparency_UsernameHashSearchKey: (hash: Uint8Array<ArrayBuffer>,) => Uint8Array<ArrayBuffer>;
|
||||
KyberKeyPair_Generate: () => KyberKeyPair;
|
||||
KyberKeyPair_GetPublicKey: (keyPair: Wrapper<KyberKeyPair>,) => KyberPublicKey;
|
||||
@ -634,6 +635,7 @@ type NativeFunctions = {
|
||||
TESTING_KeyTransChatSendError: () => void;
|
||||
TESTING_KeyTransFatalVerificationFailure: () => void;
|
||||
TESTING_KeyTransNonFatalVerificationFailure: () => void;
|
||||
TESTING_KeyTransStoredAccountData: () => Uint8Array<ArrayBuffer>;
|
||||
TESTING_NonSuspendingBackgroundThreadRuntime_New: () => NonSuspendingBackgroundThreadRuntime;
|
||||
TESTING_OtherTestingHandleType_getValue: (handle: Wrapper<OtherTestingHandleType>,) => string;
|
||||
TESTING_PanicInBodyAsync: (_input: null,) => Promise<void>;
|
||||
@ -898,6 +900,7 @@ const { registerErrors,
|
||||
KeyTransparency_AciSearchKey,
|
||||
KeyTransparency_Check,
|
||||
KeyTransparency_E164SearchKey,
|
||||
KeyTransparency_ResetDataField,
|
||||
KeyTransparency_UsernameHashSearchKey,
|
||||
KyberKeyPair_Generate,
|
||||
KyberKeyPair_GetPublicKey,
|
||||
@ -1214,6 +1217,7 @@ const { registerErrors,
|
||||
TESTING_KeyTransChatSendError,
|
||||
TESTING_KeyTransFatalVerificationFailure,
|
||||
TESTING_KeyTransNonFatalVerificationFailure,
|
||||
TESTING_KeyTransStoredAccountData,
|
||||
TESTING_NonSuspendingBackgroundThreadRuntime_New,
|
||||
TESTING_OtherTestingHandleType_getValue,
|
||||
TESTING_PanicInBodyAsync,
|
||||
@ -1479,6 +1483,7 @@ export { registerErrors,
|
||||
KeyTransparency_AciSearchKey,
|
||||
KeyTransparency_Check,
|
||||
KeyTransparency_E164SearchKey,
|
||||
KeyTransparency_ResetDataField,
|
||||
KeyTransparency_UsernameHashSearchKey,
|
||||
KyberKeyPair_Generate,
|
||||
KyberKeyPair_GetPublicKey,
|
||||
@ -1795,6 +1800,7 @@ export { registerErrors,
|
||||
TESTING_KeyTransChatSendError,
|
||||
TESTING_KeyTransFatalVerificationFailure,
|
||||
TESTING_KeyTransNonFatalVerificationFailure,
|
||||
TESTING_KeyTransStoredAccountData,
|
||||
TESTING_NonSuspendingBackgroundThreadRuntime_New,
|
||||
TESTING_OtherTestingHandleType_getValue,
|
||||
TESTING_PanicInBodyAsync,
|
||||
|
||||
@ -168,6 +168,46 @@ export interface Client {
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag identifying an optional field of the account data.
|
||||
*
|
||||
* (Must be in sync with the Rust counterpart)
|
||||
*/
|
||||
export enum AccountDataField {
|
||||
E164 = 0,
|
||||
UsernameHash = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a particular field in the data associated with given ACI.
|
||||
*
|
||||
* Must only be called for the "self" account when either E.164 or username
|
||||
* change is performed.
|
||||
*
|
||||
* Upon successful completion the data associated with the account will be
|
||||
* updated in the store, if it was present to begin with, noop if it was not.
|
||||
*
|
||||
* @param aci - An ACI of "self" account.
|
||||
* @param field - Account data field to be reset (E.164 or username hash).
|
||||
* @param store - local persistent storage for key transparency-related data.
|
||||
* @throws {TypeError} if the stored data cannot be decoded correctly, which means data corruption.
|
||||
*/
|
||||
export async function resetField(
|
||||
aci: Aci,
|
||||
field: AccountDataField,
|
||||
store: Store
|
||||
): Promise<void> {
|
||||
const accountData = await store.getAccountData(aci);
|
||||
if (accountData === null) {
|
||||
return;
|
||||
}
|
||||
const updated = Native.KeyTransparency_ResetDataField(accountData, field);
|
||||
if (updated.length === 0) {
|
||||
throw new TypeError('failed to decode account data');
|
||||
}
|
||||
await store.setAccountData(aci, updated);
|
||||
}
|
||||
|
||||
export class ClientImpl implements Client {
|
||||
constructor(
|
||||
private readonly asyncContext: TokioAsyncContext,
|
||||
|
||||
@ -90,6 +90,33 @@ describe('KeyTransparency bridging', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeyTransparency.resetField', () => {
|
||||
it('throws on corrupt data', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await store.setAccountData(testAci, new Uint8Array([1, 2, 3]));
|
||||
await expect(
|
||||
KT.resetField(testAci, KT.AccountDataField.E164, store)
|
||||
).to.be.rejectedWith(TypeError);
|
||||
});
|
||||
|
||||
it('is a noop when data is missing', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await KT.resetField(testAci, KT.AccountDataField.E164, store);
|
||||
expect(store.storage.get(testAci)).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('updates store on success', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await store.setAccountData(
|
||||
testAci,
|
||||
Native.TESTING_KeyTransStoredAccountData()
|
||||
);
|
||||
expect(store.storage.get(testAci)).to.have.lengthOf(1);
|
||||
await KT.resetField(testAci, KT.AccountDataField.E164, store);
|
||||
expect(store.storage.get(testAci)).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeyTransparency network errors', () => {
|
||||
it('can bridge network errors', async () => {
|
||||
async function run(statusCode: number, headers: string[] = []) {
|
||||
|
||||
@ -14,8 +14,8 @@ use libsignal_core::{Aci, E164};
|
||||
use libsignal_keytrans::{AccountData, StoredAccountData};
|
||||
use libsignal_net_chat::api::RequestError;
|
||||
use libsignal_net_chat::api::keytrans::{
|
||||
CheckMode, Error, KeyTransparencyClient, MaybePartial, SearchKey, TreeHeadWithTimestamp,
|
||||
UsernameHash, check,
|
||||
AccountDataField, AccountDataFieldReset as _, CheckMode, Error, KeyTransparencyClient,
|
||||
MaybePartial, SearchKey, TreeHeadWithTimestamp, UsernameHash, check,
|
||||
};
|
||||
use libsignal_protocol::PublicKey;
|
||||
use prost::{DecodeError, Message};
|
||||
@ -38,6 +38,20 @@ fn KeyTransparency_UsernameHashSearchKey(hash: &[u8]) -> Vec<u8> {
|
||||
UsernameHash::from_slice(hash).as_search_key()
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn KeyTransparency_ResetDataField(
|
||||
account_data: Box<[u8]>,
|
||||
field: AsType<AccountDataField, u8>,
|
||||
) -> Vec<u8> {
|
||||
// The only failure is decoding error, we'll use empty vec for that.
|
||||
let decoded: Result<StoredAccountData, _> = try_decode(account_data);
|
||||
let Ok(account_data) = decoded else {
|
||||
log::warn!("Failed to decode stored account data");
|
||||
return vec![];
|
||||
};
|
||||
account_data.reset(field.into_inner()).encode_to_vec()
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn KeyTransparency_Check(
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use libsignal_keytrans::{StoredAccountData, StoredMonitoringData};
|
||||
use libsignal_net_chat::api::RequestError;
|
||||
use libsignal_net_chat::api::keytrans::Error as KeyTransError;
|
||||
use prost::Message;
|
||||
|
||||
use crate::*;
|
||||
|
||||
@ -30,3 +32,23 @@ fn TESTING_KeyTransNonFatalVerificationFailure() -> Result<(), RequestError<KeyT
|
||||
fn TESTING_KeyTransChatSendError() -> Result<(), RequestError<KeyTransError>> {
|
||||
Err(RequestError::Timeout)
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_KeyTransStoredAccountData() -> Vec<u8> {
|
||||
StoredAccountData {
|
||||
aci: Some(StoredMonitoringData {
|
||||
pos: 1,
|
||||
..Default::default()
|
||||
}),
|
||||
e164: Some(StoredMonitoringData {
|
||||
pos: 2,
|
||||
..Default::default()
|
||||
}),
|
||||
username_hash: Some(StoredMonitoringData {
|
||||
pos: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
last_tree_head: None,
|
||||
}
|
||||
.encode_to_vec()
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ use libsignal_keytrans::{
|
||||
AccountData, ChatDistinguishedResponse, ChatMonitorResponse, ChatSearchResponse,
|
||||
CondensedTreeSearchResponse, FullSearchResponse, FullTreeHead, KeyTransparency, LastTreeHead,
|
||||
LocalStateUpdate, MonitorContext, MonitorKey, MonitorProof, MonitorRequest, MonitorResponse,
|
||||
SearchContext, SearchStateUpdate, SlimSearchRequest,
|
||||
SearchContext, SearchStateUpdate, SlimSearchRequest, StoredAccountData,
|
||||
};
|
||||
use libsignal_net::env::KeyTransConfig;
|
||||
use libsignal_protocol::PublicKey;
|
||||
@ -538,6 +538,20 @@ impl UnauthenticatedChatApi for KeyTransparencyClient<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AccountDataFieldReset {
|
||||
fn reset(self, field: AccountDataField) -> Self;
|
||||
}
|
||||
|
||||
impl AccountDataFieldReset for StoredAccountData {
|
||||
fn reset(mut self, field: AccountDataField) -> Self {
|
||||
match field {
|
||||
AccountDataField::E164 => self.e164 = None,
|
||||
AccountDataField::UsernameHash => self.username_hash = None,
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_support {
|
||||
use std::cell::Cell;
|
||||
@ -758,8 +772,9 @@ pub(crate) mod test_support {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
use libsignal_keytrans::StoredMonitoringData;
|
||||
use prost::Message as _;
|
||||
use test_case::test_case;
|
||||
use test_case::{test_case, test_matrix};
|
||||
|
||||
use super::test_support::{
|
||||
CHAT_SEARCH_RESPONSE, CHAT_SEARCH_RESPONSE_VALID_AT, KEYTRANS_CONFIG_STAGING, test_account,
|
||||
@ -859,4 +874,37 @@ mod test {
|
||||
assert_eq!(skip.to_vec(), missing_fields.into_iter().collect::<Vec<_>>())
|
||||
);
|
||||
}
|
||||
|
||||
#[test_matrix([AccountDataField::E164, AccountDataField::UsernameHash])]
|
||||
fn reset_account_data_field(field: AccountDataField) {
|
||||
let field_data = StoredMonitoringData::default();
|
||||
let data = StoredAccountData {
|
||||
aci: None,
|
||||
e164: Some(StoredMonitoringData {
|
||||
pos: 1,
|
||||
..field_data.clone()
|
||||
}),
|
||||
username_hash: Some(StoredMonitoringData {
|
||||
pos: 2,
|
||||
..field_data
|
||||
}),
|
||||
last_tree_head: None,
|
||||
};
|
||||
|
||||
let updated = data.clone().reset(field);
|
||||
|
||||
match field {
|
||||
AccountDataField::E164 => {
|
||||
assert!(updated.e164.is_none());
|
||||
assert_matches!(
|
||||
updated.username_hash,
|
||||
Some(StoredMonitoringData { pos: 2, .. })
|
||||
);
|
||||
}
|
||||
AccountDataField::UsernameHash => {
|
||||
assert_matches!(updated.e164, Some(StoredMonitoringData { pos: 1, .. }));
|
||||
assert!(updated.username_hash.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,16 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A tag identifying an optional field in [`libsignal_keytrans::AccountData`]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display)]
|
||||
#[repr(u8)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display, derive_more::TryFrom,
|
||||
)]
|
||||
#[try_from(repr)]
|
||||
pub enum AccountDataField {
|
||||
/// E.164
|
||||
E164,
|
||||
E164 = 0,
|
||||
/// Username hash
|
||||
UsernameHash,
|
||||
UsernameHash = 1,
|
||||
}
|
||||
|
||||
/// This struct adds to its type parameter a (potentially empty) list of
|
||||
|
||||
@ -63,6 +63,45 @@ public enum KeyTransparency {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tag identifying an optional field of the account data.
|
||||
///
|
||||
/// (Must be in sync with the Rust counterpart)
|
||||
public enum AccountDataField: UInt8 {
|
||||
case e164 = 0
|
||||
case usernameHash = 1
|
||||
}
|
||||
|
||||
/// Resets a particular field in the data associated with given ACI.
|
||||
///
|
||||
/// Must only be called for the "self" account when either E.164 or username
|
||||
/// change is performed.
|
||||
///
|
||||
/// Upon successful completion the data associated with the account will be
|
||||
/// updated in the store, if it was present to begin with, noop if it was not.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - field: Account data field to be reset (E.164 or username hash).
|
||||
/// - aci: An ACI of "self" account.
|
||||
/// - store: local persistent storage for key transparency-related data.
|
||||
/// - Throws: ``SignalError/invalidArgument(_:)`` if the stored data cannot
|
||||
/// be decoded correctly, which means data corruption.
|
||||
public static func resetField(
|
||||
_ field: AccountDataField,
|
||||
for aci: Aci,
|
||||
store: some Store
|
||||
) async throws {
|
||||
guard let accountData = await store.getAccountData(for: aci) else { return }
|
||||
let updated = try accountData.withUnsafeBorrowedBuffer { accountDataBuffer in
|
||||
try invokeFnReturningData {
|
||||
signal_key_transparency_reset_data_field($0, accountDataBuffer, field.rawValue)
|
||||
}
|
||||
}
|
||||
if updated.isEmpty {
|
||||
throw SignalError.invalidArgument("failed to decode account data")
|
||||
}
|
||||
await store.setAccountData(updated, for: aci)
|
||||
}
|
||||
|
||||
/// Typed API to access the key transparency subsystem using an existing
|
||||
/// unauthenticated chat connection.
|
||||
///
|
||||
|
||||
@ -2150,6 +2150,8 @@ SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc
|
||||
|
||||
SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164);
|
||||
|
||||
SignalFfiError *signal_key_transparency_reset_data_field(SignalOwnedBuffer *out, SignalBorrowedBuffer account_data, uint8_t field);
|
||||
|
||||
SignalFfiError *signal_key_transparency_username_hash_search_key(SignalOwnedBuffer *out, SignalBorrowedBuffer hash);
|
||||
|
||||
SignalFfiError *signal_kyber_key_pair_clone(SignalMutPointerKyberKeyPair *new_obj, SignalConstPointerKyberKeyPair obj);
|
||||
|
||||
@ -389,6 +389,8 @@ SignalFfiError *signal_testing_key_trans_fatal_verification_failure(void);
|
||||
|
||||
SignalFfiError *signal_testing_key_trans_non_fatal_verification_failure(void);
|
||||
|
||||
SignalFfiError *signal_testing_key_trans_stored_account_data(SignalOwnedBuffer *out);
|
||||
|
||||
SignalFfiError *signal_testing_other_testing_handle_type_get_value(const char **out, SignalConstPointerOtherTestingHandleType handle);
|
||||
|
||||
SignalFfiError *signal_testing_panic_in_body_async(const void *_input);
|
||||
|
||||
@ -113,6 +113,32 @@ final class KeyTransparencyTests: TestCaseBase {
|
||||
XCTAssertEqual(1, store.distinguishedTreeHeads.count)
|
||||
}
|
||||
|
||||
func testResetFieldThrowsOnCorruptData() async throws {
|
||||
let store = TestStore()
|
||||
await store.setAccountData(Data([1, 2, 3]), for: self.testAccount.aci)
|
||||
do {
|
||||
try await KeyTransparency.resetField(
|
||||
.e164,
|
||||
for: self.testAccount.aci,
|
||||
store: store
|
||||
)
|
||||
XCTFail("should have failed")
|
||||
} catch SignalError.invalidArgument(_) {
|
||||
} catch {
|
||||
XCTFail("unexpected exception thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testResetFieldIsNoopWhenDataIsMissing() async throws {
|
||||
let store = TestStore()
|
||||
try await KeyTransparency.resetField(
|
||||
.e164,
|
||||
for: self.testAccount.aci,
|
||||
store: store
|
||||
)
|
||||
XCTAssertNil(store.accountData[self.testAccount.aci])
|
||||
}
|
||||
|
||||
// These testing endpoints aren't generated in device builds, to save on code size.
|
||||
#if !os(iOS) || targetEnvironment(simulator)
|
||||
func testNonFatalErrorBridging() throws {
|
||||
@ -145,6 +171,23 @@ final class KeyTransparencyTests: TestCaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
func testResetFieldUpdatesStoreOnSuccess() async throws {
|
||||
let store = TestStore()
|
||||
let storedAccountData = failOnError {
|
||||
try invokeFnReturningData {
|
||||
signal_testing_key_trans_stored_account_data($0)
|
||||
}
|
||||
}
|
||||
await store.setAccountData(storedAccountData, for: self.testAccount.aci)
|
||||
XCTAssertEqual(1, store.accountData[self.testAccount.aci]!.count)
|
||||
try await KeyTransparency.resetField(
|
||||
.e164,
|
||||
for: self.testAccount.aci,
|
||||
store: store
|
||||
)
|
||||
XCTAssertEqual(2, store.accountData[self.testAccount.aci]!.count)
|
||||
}
|
||||
|
||||
func customNetworkErrorTestImpl(status: UInt16, headers: [String: String] = [:]) async throws {
|
||||
let tokio = TokioAsyncContext()
|
||||
let (chat, remote) = UnauthenticatedChatConnection.fakeConnect(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user