mirror of https://github.com/oxen-io/session-ios
Updated to the latest config lib and added it's unit tests
parent
22130f734e
commit
893967e380
Binary file not shown.
Binary file not shown.
@ -0,0 +1,147 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "base.h"
|
||||||
|
#include "profile_pic.h"
|
||||||
|
|
||||||
|
typedef struct contacts_contact {
|
||||||
|
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||||
|
|
||||||
|
// These can be NULL. When setting, either NULL or empty string will clear the setting.
|
||||||
|
const char* name;
|
||||||
|
const char* nickname;
|
||||||
|
user_profile_pic profile_pic;
|
||||||
|
|
||||||
|
bool approved;
|
||||||
|
bool approved_me;
|
||||||
|
bool blocked;
|
||||||
|
|
||||||
|
} contacts_contact;
|
||||||
|
|
||||||
|
/// Constructs a contacts config object and sets a pointer to it in `conf`.
|
||||||
|
///
|
||||||
|
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
|
||||||
|
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
|
||||||
|
/// bytes of that are the seed). This field cannot be null.
|
||||||
|
///
|
||||||
|
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
|
||||||
|
/// instantiation's call to `dump()`. To construct a new, empty object this should be NULL.
|
||||||
|
///
|
||||||
|
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
|
||||||
|
///
|
||||||
|
/// \param error - the pointer to a buffer in which we will write an error string if an error
|
||||||
|
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
|
||||||
|
/// buffer of at least 256 bytes.
|
||||||
|
///
|
||||||
|
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
|
||||||
|
/// C-string into `error` (if not NULL) on failure.
|
||||||
|
///
|
||||||
|
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||||
|
/// config_free() (in `session/config/base.h`).
|
||||||
|
int contacts_init(
|
||||||
|
config_object** conf,
|
||||||
|
const unsigned char* ed25519_secretkey,
|
||||||
|
const unsigned char* dump,
|
||||||
|
size_t dumplen,
|
||||||
|
char* error) __attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Returns true if session_id has the right form (66 hex digits). This is a quick check, not a
|
||||||
|
/// robust one: it does not check the leading byte prefix, nor the cryptographic properties of the
|
||||||
|
/// pubkey for actual validity.
|
||||||
|
bool session_id_is_valid(const char* session_id);
|
||||||
|
|
||||||
|
/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex
|
||||||
|
/// string), if the contact exists, and returns true. If the contact does not exist then `contact`
|
||||||
|
/// is left unchanged and false is returned.
|
||||||
|
bool contacts_get(const config_object* conf, contacts_contact* contact, const char* session_id)
|
||||||
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Same as the above except that when the contact does not exist, this sets all the contact fields
|
||||||
|
/// to defaults and loads it with the given session_id.
|
||||||
|
///
|
||||||
|
/// Returns true as long as it is given a valid session_id. A false return is considered an error,
|
||||||
|
/// and means the session_id was not a valid session_id.
|
||||||
|
///
|
||||||
|
/// This is the method that should usually be used to create or update a contact, followed by
|
||||||
|
/// setting fields in the contact, and then giving it to contacts_set().
|
||||||
|
bool contacts_get_or_create(
|
||||||
|
const config_object* conf, contacts_contact* contact, const char* session_id)
|
||||||
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Adds or updates a contact from the given contact info struct.
|
||||||
|
void contacts_set(config_object* conf, const contacts_contact* contact);
|
||||||
|
|
||||||
|
// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would
|
||||||
|
// save very little in actual calling code. The procedure for updating a single field without them
|
||||||
|
// is simple enough; for example to update `approved` and leave everything else unchanged:
|
||||||
|
//
|
||||||
|
// contacts_contact c;
|
||||||
|
// if (contacts_get_or_create(conf, &c, some_session_id)) {
|
||||||
|
// const char* new_nickname = "Joe";
|
||||||
|
// c.approved = new_nickname;
|
||||||
|
// contacts_set_or_create(conf, &c);
|
||||||
|
// } else {
|
||||||
|
// // some_session_id was invalid!
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was
|
||||||
|
/// found and removed, false if the contact was not present. You must not call this during
|
||||||
|
/// iteration; see details below.
|
||||||
|
bool contacts_erase(config_object* conf, const char* session_id);
|
||||||
|
|
||||||
|
/// Functions for iterating through the entire contact list, in sorted order. Intended use is:
|
||||||
|
///
|
||||||
|
/// contacts_contact c;
|
||||||
|
/// contacts_iterator *it = contacts_iterator_new(contacts);
|
||||||
|
/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) {
|
||||||
|
/// // c.session_id, c.nickname, etc. are loaded
|
||||||
|
/// }
|
||||||
|
/// contacts_iterator_free(it);
|
||||||
|
///
|
||||||
|
/// It is permitted to modify records (e.g. with a call to `contacts_set`) and add records while
|
||||||
|
/// iterating.
|
||||||
|
///
|
||||||
|
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||||
|
/// iteration by calling either contacts_iterator_advance if not deleting, or
|
||||||
|
/// contacts_iterator_erase to erase and advance. Usage looks like this:
|
||||||
|
///
|
||||||
|
/// contacts_contact c;
|
||||||
|
/// contacts_iterator *it = contacts_iterator_new(contacts);
|
||||||
|
/// while (!contacts_iterator_done(it, &c)) {
|
||||||
|
/// // c.session_id, c.nickname, etc. are loaded
|
||||||
|
///
|
||||||
|
/// bool should_delete = /* ... */;
|
||||||
|
///
|
||||||
|
/// if (should_delete)
|
||||||
|
/// contacts_iterator_erase(it);
|
||||||
|
/// else
|
||||||
|
/// contacts_iterator_advance(it);
|
||||||
|
/// }
|
||||||
|
/// contacts_iterator_free(it);
|
||||||
|
///
|
||||||
|
///
|
||||||
|
|
||||||
|
typedef struct contacts_iterator {
|
||||||
|
void* _internals;
|
||||||
|
} contacts_iterator;
|
||||||
|
|
||||||
|
// Starts a new iterator.
|
||||||
|
contacts_iterator* contacts_iterator_new(const config_object* conf);
|
||||||
|
// Frees an iterator once no longer needed.
|
||||||
|
void contacts_iterator_free(contacts_iterator* it);
|
||||||
|
|
||||||
|
// Returns true if iteration has reached the end. Otherwise `c` is populated and false is returned.
|
||||||
|
bool contacts_iterator_done(contacts_iterator* it, contacts_contact* c);
|
||||||
|
|
||||||
|
// Advances the iterator.
|
||||||
|
void contacts_iterator_advance(contacts_iterator* it);
|
||||||
|
|
||||||
|
// Erases the current contact while advancing the iterator to the next contact in the iteration.
|
||||||
|
void contacts_iterator_erase(config_object* conf, contacts_iterator* it);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
@ -0,0 +1,187 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <session/config.hpp>
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "namespaces.hpp"
|
||||||
|
#include "profile_pic.hpp"
|
||||||
|
|
||||||
|
extern "C" struct contacts_contact;
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
|
||||||
|
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||||
|
///
|
||||||
|
/// c - dict of contacts; within this dict each key is the session pubkey (binary, 33 bytes) and
|
||||||
|
/// value is a dict containing keys:
|
||||||
|
///
|
||||||
|
/// ! - dummy value that is always set to an empty string. This ensures that we always have at
|
||||||
|
/// least one key set, which is required to keep the dict value alive (empty dicts get
|
||||||
|
/// pruned when serialied).
|
||||||
|
/// n - contact name (string)
|
||||||
|
/// N - contact nickname (string)
|
||||||
|
/// p - profile url (string)
|
||||||
|
/// q - profile decryption key (binary)
|
||||||
|
/// a - 1 if approved, omitted otherwise (int)
|
||||||
|
/// A - 1 if remote has approved me, omitted otherwise (int)
|
||||||
|
/// b - 1 if contact is blocked, omitted otherwise
|
||||||
|
|
||||||
|
/// Struct containing contact info. Note that data must be copied/used immediately as the data will
|
||||||
|
/// not remain valid beyond other calls into the library. When settings things in this externally
|
||||||
|
/// (e.g. to pass into `set()`), take note that the `name` and `nickname` are string_views: that is,
|
||||||
|
/// they must reference existing string data that remains valid for the duration of the contact_info
|
||||||
|
/// instance.
|
||||||
|
struct contact_info {
|
||||||
|
std::string session_id; // in hex
|
||||||
|
std::optional<std::string_view> name;
|
||||||
|
std::optional<std::string_view> nickname;
|
||||||
|
std::optional<profile_pic> profile_picture;
|
||||||
|
bool approved = false;
|
||||||
|
bool approved_me = false;
|
||||||
|
bool blocked = false;
|
||||||
|
|
||||||
|
contact_info(std::string sid);
|
||||||
|
|
||||||
|
// Internal ctor/method for C API implementations:
|
||||||
|
contact_info(const struct contacts_contact& c); // From c struct
|
||||||
|
void into(contacts_contact& c); // Into c struct
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Contacts;
|
||||||
|
|
||||||
|
void load(const dict& info_dict);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Contacts : public ConfigBase {
|
||||||
|
|
||||||
|
public:
|
||||||
|
// No default constructor
|
||||||
|
Contacts() = delete;
|
||||||
|
|
||||||
|
/// Constructs a contact list from existing data (stored from `dump()`) and the user's secret
|
||||||
|
/// key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||||
|
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||||
|
///
|
||||||
|
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||||
|
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||||
|
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||||
|
/// the secret key.
|
||||||
|
///
|
||||||
|
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||||
|
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||||
|
Contacts(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||||
|
|
||||||
|
Namespace storage_namespace() const override { return Namespace::Contacts; }
|
||||||
|
|
||||||
|
const char* encryption_domain() const override { return "Contacts"; }
|
||||||
|
|
||||||
|
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
||||||
|
/// not found, otherwise returns a filled out `contact_info`.
|
||||||
|
std::optional<contact_info> get(std::string_view pubkey_hex) const;
|
||||||
|
|
||||||
|
/// Similar to get(), but if the session ID does not exist this returns a filled-out
|
||||||
|
/// contact_info containing the session_id (all other fields will be empty/defaulted). This is
|
||||||
|
/// intended to be combined with `set` to set-or-create a record. Note that this does not add
|
||||||
|
/// the session id to the contact list when called: that requires also calling `set` with this
|
||||||
|
/// value.
|
||||||
|
contact_info get_or_create(std::string_view pubkey_hex) const;
|
||||||
|
|
||||||
|
/// Sets or updates multiple contact info values at once with the given info. The usual use is
|
||||||
|
/// to access the current info, change anything desired, then pass it back into set_contact,
|
||||||
|
/// e.g.:
|
||||||
|
///
|
||||||
|
/// auto c = contacts.get_or_create(pubkey);
|
||||||
|
/// c.name = "Session User 42";
|
||||||
|
/// c.nickname = "BFF";
|
||||||
|
/// contacts.set(c);
|
||||||
|
void set(const contact_info& contact);
|
||||||
|
|
||||||
|
/// Alternative to `set()` for setting individual fields.
|
||||||
|
void set_name(std::string_view session_id, std::string_view name);
|
||||||
|
void set_nickname(std::string_view session_id, std::string_view nickname);
|
||||||
|
void set_profile_pic(std::string_view session_id, profile_pic pic);
|
||||||
|
void set_approved(std::string_view session_id, bool approved);
|
||||||
|
void set_approved_me(std::string_view session_id, bool approved_me);
|
||||||
|
void set_blocked(std::string_view session_id, bool blocked);
|
||||||
|
|
||||||
|
/// Removes a contact, if present. Returns true if it was found and removed, false otherwise.
|
||||||
|
/// Note that this removes all fields related to a contact, even fields we do not know about.
|
||||||
|
bool erase(std::string_view session_id);
|
||||||
|
|
||||||
|
struct iterator;
|
||||||
|
|
||||||
|
/// This works like erase, but takes an iterator to the contact to remove. The element is
|
||||||
|
/// removed and the iterator to the next element after the removed one is returned. This is
|
||||||
|
/// intended for use where elements are to be removed during iteration: see below for an
|
||||||
|
/// example.
|
||||||
|
iterator erase(iterator it);
|
||||||
|
|
||||||
|
/// Iterators for iterating through all contacts. Typically you access this implicit via a for
|
||||||
|
/// loop over the `Contacts` object:
|
||||||
|
///
|
||||||
|
/// for (auto& contact : contacts) {
|
||||||
|
/// // use contact.session_id, contact.name, etc.
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// This iterates in sorted order through the session_ids.
|
||||||
|
///
|
||||||
|
/// It is permitted to modify and add records while iterating (e.g. by modifying `contact` and
|
||||||
|
/// then calling set()).
|
||||||
|
///
|
||||||
|
/// If you need to erase the current contact during iteration then care is required: you need to
|
||||||
|
/// advance the iterator via the iterator version of erase when erasing an element rather than
|
||||||
|
/// incrementing it regularly. For example:
|
||||||
|
///
|
||||||
|
/// for (auto it = contacts.begin(); it != contacts.end(); ) {
|
||||||
|
/// if (should_remove(*it))
|
||||||
|
/// it = contacts.erase(it);
|
||||||
|
/// else
|
||||||
|
/// ++it;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||||
|
/// contacts doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||||
|
/// through that vector calling `erase()` for each one.
|
||||||
|
///
|
||||||
|
iterator begin() const { return iterator{data["c"].dict()}; }
|
||||||
|
iterator end() const { return iterator{nullptr}; }
|
||||||
|
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using value_type = contact_info;
|
||||||
|
using reference = value_type&;
|
||||||
|
using pointer = value_type*;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
|
||||||
|
struct iterator {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<contact_info> _val;
|
||||||
|
dict::const_iterator _it;
|
||||||
|
const dict* _contacts;
|
||||||
|
void _load_info();
|
||||||
|
iterator(const dict* contacts) : _contacts{contacts} {
|
||||||
|
if (_contacts) {
|
||||||
|
_it = _contacts->begin();
|
||||||
|
_load_info();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
friend class Contacts;
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool operator==(const iterator& other) const;
|
||||||
|
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||||
|
bool done() const; // Equivalent to comparing against the end iterator
|
||||||
|
contact_info& operator*() const { return *_val; }
|
||||||
|
contact_info* operator->() const { return _val.get(); }
|
||||||
|
iterator& operator++();
|
||||||
|
iterator operator++(int) {
|
||||||
|
auto copy{*this};
|
||||||
|
++*this;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace session::config
|
@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
typedef struct user_profile_pic {
|
||||||
|
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
|
||||||
|
// profile pic.
|
||||||
|
const char* url;
|
||||||
|
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
|
||||||
|
// null-terminated C string. Will be NULL if there is no profile pic.
|
||||||
|
const unsigned char* key;
|
||||||
|
size_t keylen;
|
||||||
|
} user_profile_pic;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "session/types.hpp"
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
// Profile pic info. Note that `url` is null terminated (though the null lies just beyond the end
|
||||||
|
// of the string view: that is, it views into a full std::string).
|
||||||
|
struct profile_pic {
|
||||||
|
std::string_view url;
|
||||||
|
ustring_view key;
|
||||||
|
|
||||||
|
profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} {}
|
||||||
|
|
||||||
|
// Returns true if either url or key are empty
|
||||||
|
bool empty() const { return url.empty() || key.empty(); }
|
||||||
|
|
||||||
|
// Guard against accidentally passing in a temporary string or ustring:
|
||||||
|
template <
|
||||||
|
typename UrlType,
|
||||||
|
typename KeyType,
|
||||||
|
std::enable_if_t<
|
||||||
|
std::is_same_v<UrlType, std::string> || std::is_same_v<KeyType, ustring>>>
|
||||||
|
profile_pic(UrlType&& url, KeyType&& key) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace session::config
|
@ -0,0 +1,324 @@
|
|||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Sodium
|
||||||
|
import SessionUtil
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
import Quick
|
||||||
|
import Nimble
|
||||||
|
|
||||||
|
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
|
||||||
|
class ConfigContactsSpec: QuickSpec {
|
||||||
|
// MARK: - Spec
|
||||||
|
|
||||||
|
override func spec() {
|
||||||
|
it("generates Contact configs correctly") {
|
||||||
|
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
|
||||||
|
|
||||||
|
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
|
||||||
|
let identity = try! Identity.generate(from: seed)
|
||||||
|
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
|
||||||
|
expect(edSK.toHexString().suffix(64))
|
||||||
|
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
|
||||||
|
expect(identity.x25519KeyPair.publicKey.toHexString())
|
||||||
|
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
|
||||||
|
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
|
||||||
|
|
||||||
|
// Initialize a brand new, empty config because we have no dump data to deal with.
|
||||||
|
let error: UnsafeMutablePointer<CChar>? = nil
|
||||||
|
var conf: UnsafeMutablePointer<config_object>? = nil
|
||||||
|
expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0))
|
||||||
|
error?.deallocate()
|
||||||
|
|
||||||
|
// Empty contacts shouldn't have an existing contact
|
||||||
|
var definitelyRealId: [CChar] = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
|
||||||
|
expect(contacts_get(conf, contactPtr, &definitelyRealId)).to(beFalse())
|
||||||
|
|
||||||
|
var contact2: contacts_contact = contacts_contact()
|
||||||
|
expect(contacts_get_or_create(conf, &contact2, &definitelyRealId)).to(beTrue())
|
||||||
|
expect(contact2.name).to(beNil())
|
||||||
|
expect(contact2.nickname).to(beNil())
|
||||||
|
expect(contact2.approved).to(beFalse())
|
||||||
|
expect(contact2.approved_me).to(beFalse())
|
||||||
|
expect(contact2.blocked).to(beFalse())
|
||||||
|
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
|
expect(contact2.profile_pic.url).to(beNil())
|
||||||
|
expect(contact2.profile_pic.key).to(beNil())
|
||||||
|
expect(contact2.profile_pic.keylen).to(equal(0))
|
||||||
|
|
||||||
|
// We don't need to push anything, since this is a default contact
|
||||||
|
expect(config_needs_push(conf)).to(beFalse())
|
||||||
|
// And we haven't changed anything so don't need to dump to db
|
||||||
|
expect(config_needs_dump(conf)).to(beFalse())
|
||||||
|
|
||||||
|
var toPush: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPushLen: Int = 0
|
||||||
|
// We don't need to push since we haven't changed anything, so this call is mainly just for
|
||||||
|
// testing:
|
||||||
|
let seqno: Int64 = config_push(conf, &toPush, &toPushLen)
|
||||||
|
expect(toPush).toNot(beNil())
|
||||||
|
expect(seqno).to(equal(0))
|
||||||
|
expect(toPushLen).to(equal(256))
|
||||||
|
|
||||||
|
// Update the contact data
|
||||||
|
let contact2Name: [CChar] = "Joe"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
let contact2Nickname: [CChar] = "Joey"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
contact2Name.withUnsafeBufferPointer { contact2.name = $0.baseAddress }
|
||||||
|
contact2Nickname.withUnsafeBufferPointer { contact2.nickname = $0.baseAddress }
|
||||||
|
contact2.approved = true
|
||||||
|
contact2.approved_me = true
|
||||||
|
|
||||||
|
// Update the contact
|
||||||
|
contacts_set(conf, &contact2)
|
||||||
|
|
||||||
|
// Ensure the contact details were updated
|
||||||
|
var contact3: contacts_contact = contacts_contact()
|
||||||
|
expect(contacts_get(conf, &contact3, &definitelyRealId)).to(beTrue())
|
||||||
|
expect(String(cString: contact3.name)).to(equal("Joe"))
|
||||||
|
expect(String(cString: contact3.nickname)).to(equal("Joey"))
|
||||||
|
expect(contact3.approved).to(beTrue())
|
||||||
|
expect(contact3.approved_me).to(beTrue())
|
||||||
|
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
|
expect(contact3.profile_pic.url).to(beNil())
|
||||||
|
expect(contact3.profile_pic.key).to(beNil())
|
||||||
|
expect(contact3.profile_pic.keylen).to(equal(0))
|
||||||
|
expect(contact3.blocked).to(beFalse())
|
||||||
|
|
||||||
|
let contact3SessionId: [CChar] = withUnsafeBytes(of: contact3.session_id) { [UInt8]($0) }
|
||||||
|
.map { CChar($0) }
|
||||||
|
expect(contact3SessionId).to(equal(definitelyRealId.nullTerminated()))
|
||||||
|
|
||||||
|
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||||
|
// to dump the updated state:
|
||||||
|
expect(config_needs_push(conf)).to(beTrue())
|
||||||
|
expect(config_needs_dump(conf)).to(beTrue())
|
||||||
|
|
||||||
|
var toPush2: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush2Len: Int = 0
|
||||||
|
let seqno2: Int64 = config_push(conf, &toPush2, &toPush2Len);
|
||||||
|
// incremented since we made changes (this only increments once between
|
||||||
|
// dumps; even though we changed multiple fields here).
|
||||||
|
expect(seqno2).to(equal(1))
|
||||||
|
toPush2?.deallocate()
|
||||||
|
|
||||||
|
// Pretend we uploaded it
|
||||||
|
config_confirm_pushed(conf, seqno2)
|
||||||
|
expect(config_needs_push(conf)).to(beFalse())
|
||||||
|
expect(config_needs_dump(conf)).to(beTrue())
|
||||||
|
|
||||||
|
// NB: Not going to check encrypted data and decryption here because that's general (not
|
||||||
|
// specific to contacts) and is covered already in the user profile tests.
|
||||||
|
var dump1: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var dump1Len: Int = 0
|
||||||
|
config_dump(conf, &dump1, &dump1Len)
|
||||||
|
|
||||||
|
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||||
|
var conf2: UnsafeMutablePointer<config_object>? = nil
|
||||||
|
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
|
||||||
|
error?.deallocate()
|
||||||
|
dump1?.deallocate()
|
||||||
|
|
||||||
|
expect(config_needs_push(conf2)).to(beFalse())
|
||||||
|
expect(config_needs_dump(conf2)).to(beFalse())
|
||||||
|
|
||||||
|
var toPush3: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush3Len: Int = 0
|
||||||
|
let seqno3: Int64 = config_push(conf, &toPush3, &toPush3Len);
|
||||||
|
expect(seqno3).to(equal(1))
|
||||||
|
toPush3?.deallocate()
|
||||||
|
|
||||||
|
// Because we just called dump() above, to load up contacts2
|
||||||
|
expect(config_needs_dump(conf)).to(beFalse())
|
||||||
|
|
||||||
|
// Ensure the contact details were updated
|
||||||
|
var contact4: contacts_contact = contacts_contact()
|
||||||
|
expect(contacts_get(conf2, &contact4, &definitelyRealId)).to(beTrue())
|
||||||
|
expect(String(cString: contact4.name)).to(equal("Joe"))
|
||||||
|
expect(String(cString: contact4.nickname)).to(equal("Joey"))
|
||||||
|
expect(contact4.approved).to(beTrue())
|
||||||
|
expect(contact4.approved_me).to(beTrue())
|
||||||
|
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
|
expect(contact4.profile_pic.url).to(beNil())
|
||||||
|
expect(contact4.profile_pic.key).to(beNil())
|
||||||
|
expect(contact4.profile_pic.keylen).to(equal(0))
|
||||||
|
expect(contact4.blocked).to(beFalse())
|
||||||
|
|
||||||
|
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
var contact5: contacts_contact = contacts_contact()
|
||||||
|
expect(contacts_get_or_create(conf2, &contact5, &anotherId)).to(beTrue())
|
||||||
|
expect(contact5.name).to(beNil())
|
||||||
|
expect(contact5.nickname).to(beNil())
|
||||||
|
expect(contact5.approved).to(beFalse())
|
||||||
|
expect(contact5.approved_me).to(beFalse())
|
||||||
|
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
|
expect(contact5.profile_pic.url).to(beNil())
|
||||||
|
expect(contact5.profile_pic.key).to(beNil())
|
||||||
|
expect(contact5.profile_pic.keylen).to(equal(0))
|
||||||
|
expect(contact5.blocked).to(beFalse())
|
||||||
|
|
||||||
|
// We're not setting any fields, but we should still keep a record of the session id
|
||||||
|
contacts_set(conf2, &contact5)
|
||||||
|
expect(config_needs_push(conf2)).to(beTrue())
|
||||||
|
|
||||||
|
var toPush4: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush4Len: Int = 0
|
||||||
|
let seqno4: Int64 = config_push(conf2, &toPush4, &toPush4Len);
|
||||||
|
expect(seqno4).to(equal(2))
|
||||||
|
|
||||||
|
// Check the merging
|
||||||
|
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush4)]
|
||||||
|
var mergeSize: [Int] = [toPush4Len]
|
||||||
|
expect(config_merge(conf, &mergeData, &mergeSize, 1)).to(equal(1))
|
||||||
|
config_confirm_pushed(conf2, seqno4)
|
||||||
|
toPush4?.deallocate()
|
||||||
|
|
||||||
|
expect(config_needs_push(conf)).to(beFalse())
|
||||||
|
|
||||||
|
var toPush5: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush5Len: Int = 0
|
||||||
|
let seqno5: Int64 = config_push(conf2, &toPush5, &toPush5Len);
|
||||||
|
expect(seqno5).to(equal(2))
|
||||||
|
toPush5?.deallocate()
|
||||||
|
|
||||||
|
// Iterate through and make sure we got everything we expected
|
||||||
|
var sessionIds: [String] = []
|
||||||
|
var nicknames: [String] = []
|
||||||
|
var contact6: contacts_contact = contacts_contact()
|
||||||
|
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
||||||
|
while !contacts_iterator_done(contactIterator, &contact6) {
|
||||||
|
sessionIds.append(
|
||||||
|
String(cString: withUnsafeBytes(of: contact6.session_id) { [UInt8]($0) }
|
||||||
|
.map { CChar($0) }
|
||||||
|
.nullTerminated()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
nicknames.append(
|
||||||
|
contact6.nickname.map { String(cString: $0) } ??
|
||||||
|
"(N/A)"
|
||||||
|
)
|
||||||
|
contacts_iterator_advance(contactIterator)
|
||||||
|
}
|
||||||
|
contacts_iterator_free(contactIterator) // Need to free the iterator
|
||||||
|
|
||||||
|
expect(sessionIds.count).to(equal(2))
|
||||||
|
expect(sessionIds.first).to(equal(String(cString: definitelyRealId.nullTerminated())))
|
||||||
|
expect(sessionIds.last).to(equal(String(cString: anotherId.nullTerminated())))
|
||||||
|
expect(nicknames.first).to(equal("Joey"))
|
||||||
|
expect(nicknames.last).to(equal("(N/A)"))
|
||||||
|
|
||||||
|
// Conflict! Oh no!
|
||||||
|
|
||||||
|
// On client 1 delete a contact:
|
||||||
|
contacts_erase(conf, definitelyRealId)
|
||||||
|
|
||||||
|
// Client 2 adds a new friend:
|
||||||
|
var thirdId: [CChar] = "052222222222222222222222222222222222222222222222222222222222222222"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
let nickname7: [CChar] = "Nickname 3"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
let profileUrl7: [CChar] = "http://example.com/huge.bmp"
|
||||||
|
.bytes
|
||||||
|
.map { CChar(bitPattern: $0) }
|
||||||
|
let profileKey7: [UInt8] = "qwerty".bytes
|
||||||
|
var contact7: contacts_contact = contacts_contact()
|
||||||
|
expect(contacts_get_or_create(conf2, &contact7, &thirdId)).to(beTrue())
|
||||||
|
nickname7.withUnsafeBufferPointer { contact7.nickname = $0.baseAddress }
|
||||||
|
contact7.approved = true
|
||||||
|
contact7.approved_me = true
|
||||||
|
profileUrl7.withUnsafeBufferPointer { contact7.profile_pic.url = $0.baseAddress }
|
||||||
|
profileKey7.withUnsafeBufferPointer { contact7.profile_pic.key = $0.baseAddress }
|
||||||
|
contact7.profile_pic.keylen = 6
|
||||||
|
contacts_set(conf2, &contact7)
|
||||||
|
|
||||||
|
expect(config_needs_push(conf)).to(beTrue())
|
||||||
|
expect(config_needs_push(conf2)).to(beTrue())
|
||||||
|
|
||||||
|
var toPush6: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush6Len: Int = 0
|
||||||
|
let seqno6: Int64 = config_push(conf, &toPush6, &toPush6Len);
|
||||||
|
expect(seqno6).to(equal(3))
|
||||||
|
|
||||||
|
var toPush7: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush7Len: Int = 0
|
||||||
|
let seqno7: Int64 = config_push(conf2, &toPush7, &toPush7Len);
|
||||||
|
expect(seqno7).to(equal(3))
|
||||||
|
|
||||||
|
expect(String(pointer: toPush6, length: toPush6Len, encoding: .ascii))
|
||||||
|
.toNot(equal(String(pointer: toPush7, length: toPush7Len, encoding: .ascii)))
|
||||||
|
|
||||||
|
config_confirm_pushed(conf, seqno6)
|
||||||
|
config_confirm_pushed(conf2, seqno7)
|
||||||
|
|
||||||
|
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush7)]
|
||||||
|
var mergeSize2: [Int] = [toPush7Len]
|
||||||
|
expect(config_merge(conf, &mergeData2, &mergeSize2, 1)).to(equal(1))
|
||||||
|
expect(config_needs_push(conf)).to(beTrue())
|
||||||
|
|
||||||
|
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush6)]
|
||||||
|
var mergeSize3: [Int] = [toPush6Len]
|
||||||
|
expect(config_merge(conf2, &mergeData3, &mergeSize3, 1)).to(equal(1))
|
||||||
|
expect(config_needs_push(conf2)).to(beTrue())
|
||||||
|
toPush6?.deallocate()
|
||||||
|
toPush7?.deallocate()
|
||||||
|
|
||||||
|
var toPush8: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush8Len: Int = 0
|
||||||
|
let seqno8: Int64 = config_push(conf, &toPush8, &toPush8Len);
|
||||||
|
expect(seqno8).to(equal(4))
|
||||||
|
|
||||||
|
var toPush9: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
var toPush9Len: Int = 0
|
||||||
|
let seqno9: Int64 = config_push(conf2, &toPush9, &toPush9Len);
|
||||||
|
expect(seqno9).to(equal(seqno8))
|
||||||
|
|
||||||
|
expect(String(pointer: toPush8, length: toPush8Len, encoding: .ascii))
|
||||||
|
.to(equal(String(pointer: toPush9, length: toPush9Len, encoding: .ascii)))
|
||||||
|
toPush8?.deallocate()
|
||||||
|
toPush9?.deallocate()
|
||||||
|
|
||||||
|
config_confirm_pushed(conf, seqno8)
|
||||||
|
config_confirm_pushed(conf2, seqno9)
|
||||||
|
|
||||||
|
expect(config_needs_push(conf)).to(beFalse())
|
||||||
|
expect(config_needs_push(conf2)).to(beFalse())
|
||||||
|
|
||||||
|
// Validate the changes
|
||||||
|
var sessionIds2: [String] = []
|
||||||
|
var nicknames2: [String] = []
|
||||||
|
var contact8: contacts_contact = contacts_contact()
|
||||||
|
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
||||||
|
while !contacts_iterator_done(contactIterator2, &contact8) {
|
||||||
|
sessionIds2.append(
|
||||||
|
String(cString: withUnsafeBytes(of: contact8.session_id) { [UInt8]($0) }
|
||||||
|
.map { CChar($0) }
|
||||||
|
.nullTerminated()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
nicknames2.append(
|
||||||
|
contact8.nickname.map { String(cString: $0) } ??
|
||||||
|
"(N/A)"
|
||||||
|
)
|
||||||
|
contacts_iterator_advance(contactIterator2)
|
||||||
|
}
|
||||||
|
contacts_iterator_free(contactIterator2) // Need to free the iterator
|
||||||
|
|
||||||
|
expect(sessionIds2.count).to(equal(2))
|
||||||
|
expect(sessionIds2.first).to(equal(String(cString: anotherId.nullTerminated())))
|
||||||
|
expect(sessionIds2.last).to(equal(String(cString: thirdId.nullTerminated())))
|
||||||
|
expect(nicknames2.first).to(equal("(N/A)"))
|
||||||
|
expect(nicknames2.last).to(equal("Nickname 3"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue