• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2214

10 Apr 2024 11:21PM UTC coverage: 91.813% (-0.8%) from 92.623%
2214

push

Evergreen

web-flow
Add missing availability checks for SecCopyErrorMessageString (#7577)

This requires iOS 11.3 and we currently target iOS 11.

94848 of 175770 branches covered (53.96%)

7 of 22 new or added lines in 2 files covered. (31.82%)

1815 existing lines in 77 files now uncovered.

242945 of 264608 relevant lines covered (91.81%)

6136478.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.42
/test/object-store/sync/metadata.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2024 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/list.hpp>
20
#include <realm/object-store/impl/apple/keychain_helper.hpp>
21
#include <realm/object-store/shared_realm.hpp>
22
#include <realm/object-store/sync/impl/app_metadata.hpp>
23
#include <realm/object-store/sync/impl/sync_file.hpp>
24
#include <realm/util/file.hpp>
25
#include <realm/util/scope_exit.hpp>
26

27
#include <util/test_path.hpp>
28
#include <util/test_utils.hpp>
29

30
#include <iostream>
31

32
#if REALM_PLATFORM_APPLE
33
#include <realm/util/cf_str.hpp>
34
#include <Security/Security.h>
35
#endif
36

37
using namespace realm;
38
using namespace realm::app;
39
using realm::util::File;
40

41
namespace {
42
const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir";
43
const std::string metadata_path = base_path + "/mongodb-realm/app%20id/server-utility/metadata/sync_metadata.realm";
44
constexpr const char* user_id = "user_id";
45
constexpr const char* device_id = "device_id";
46
constexpr const char* app_id = "app id";
47
const auto access_token = encode_fake_jwt("access_token", 123, 456);
48
const auto refresh_token = encode_fake_jwt("refresh_token", 123, 456);
49

50
std::shared_ptr<Realm> get_metadata_realm()
51
{
8✔
52
    RealmConfig realm_config;
8✔
53
    realm_config.automatic_change_notifications = false;
8✔
54
    realm_config.path = metadata_path;
8✔
55
    return Realm::get_shared_realm(std::move(realm_config));
8✔
56
}
8✔
57

58
#if REALM_PLATFORM_APPLE
59
using realm::util::adoptCF;
60
using realm::util::CFPtr;
61

62
constexpr const char* access_group = "";
63
bool can_access_keychain()
64
{
3✔
65
    static bool can_access_keychain = [] {
1✔
66
        bool can_access = keychain::create_new_metadata_realm_key(app_id, access_group) != none;
1✔
67
        if (can_access) {
1✔
68
            keychain::delete_metadata_realm_encryption_key(app_id, access_group);
69
        }
70
        else {
1✔
71
            std::cout << "Skipping keychain tests as the keychain is not accessible\n";
1✔
72
        }
1✔
73
        return can_access;
1✔
74
    }();
1✔
75
    return can_access_keychain;
3✔
76
}
3✔
77

78
CFPtr<CFMutableDictionaryRef> build_search_dictionary(CFStringRef account, CFStringRef service)
79
{
80
    auto d = adoptCF(
81
        CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
82
    CFDictionaryAddValue(d.get(), kSecClass, kSecClassGenericPassword);
83
    CFDictionaryAddValue(d.get(), kSecReturnData, kCFBooleanTrue);
84
    CFDictionaryAddValue(d.get(), kSecAttrAccount, account);
85
    CFDictionaryAddValue(d.get(), kSecAttrService, service);
86
    return d;
87
}
88

89
OSStatus get_key(CFStringRef account, CFStringRef service, std::vector<char>& result)
90
{
91
    auto search_dictionary = build_search_dictionary(account, service);
92
    CFDataRef retained_key_data;
93
    OSStatus status = SecItemCopyMatching(search_dictionary.get(), (CFTypeRef*)&retained_key_data);
94
    if (status == errSecSuccess) {
×
95
        CFPtr<CFDataRef> key_data = adoptCF(retained_key_data);
96
        auto key_bytes = reinterpret_cast<const char*>(CFDataGetBytePtr(key_data.get()));
97
        result.assign(key_bytes, key_bytes + CFDataGetLength(key_data.get()));
98
    }
99
    return status;
100
}
101

102
OSStatus set_key(const std::vector<char>& key, CFStringRef account, CFStringRef service)
103
{
104
    auto search_dictionary = build_search_dictionary(account, service);
105
    CFDictionaryAddValue(search_dictionary.get(), kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock);
106
    auto key_data = adoptCF(CFDataCreateWithBytesNoCopy(nullptr, reinterpret_cast<const UInt8*>(key.data()),
107
                                                        key.size(), kCFAllocatorNull));
108
    CFDictionaryAddValue(search_dictionary.get(), kSecValueData, key_data.get());
109
    return SecItemAdd(search_dictionary.get(), nullptr);
110
}
111

112
std::vector<char> generate_key()
113
{
114
    std::vector<char> key(64);
115
    arc4random_buf(key.data(), key.size());
116
    return key;
117
}
118
#endif // REALM_PLATFORM_APPLE
119
} // anonymous namespace
120

121
namespace realm::app {
122
static std::ostream& operator<<(std::ostream& os, AppConfig::MetadataMode mode)
123
{
70✔
124
    switch (mode) {
70✔
125
        case AppConfig::MetadataMode::InMemory:
35✔
126
            os << "InMemory";
35✔
127
            break;
35✔
128
        case AppConfig::MetadataMode::NoEncryption:
35✔
129
            os << "NoEncryption";
35✔
130
            break;
35✔
UNCOV
131
        case AppConfig::MetadataMode::Encryption:
✔
UNCOV
132
            os << "Encryption";
×
UNCOV
133
            break;
×
UNCOV
134
        default:
✔
UNCOV
135
            os << "unknown";
×
UNCOV
136
            break;
×
137
    }
70✔
138
    return os;
70✔
139
}
70✔
140
} // namespace realm::app
141

142
using Strings = std::vector<std::string>;
143

144
TEST_CASE("app metadata: common", "[sync][metadata]") {
70✔
145
    test_util::TestDirGuard test_dir(base_path);
70✔
146

34✔
147
    AppConfig config;
70✔
148
    config.app_id = app_id;
70✔
149
    config.metadata_mode = GENERATE(AppConfig::MetadataMode::InMemory, AppConfig::MetadataMode::NoEncryption);
70✔
150
    config.base_file_path = base_path;
70✔
151
    SyncFileManager file_manager(config);
70✔
152
    auto store = create_metadata_store(config, file_manager);
70✔
153

34✔
154
    INFO(config.metadata_mode);
70✔
155

34✔
156
    SECTION("create_user() creates new logged-in users") {
70✔
157
        REQUIRE_FALSE(store->has_logged_in_user(user_id));
4!
158
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
159
        REQUIRE(store->has_logged_in_user(user_id));
4!
160
        auto data = store->get_user(user_id);
4✔
161
        REQUIRE(data);
4!
162
        REQUIRE(data->access_token.token == access_token);
4!
163
        REQUIRE(data->refresh_token.token == refresh_token);
4!
164
        REQUIRE(data->device_id == device_id);
4!
165
    }
4✔
166

34✔
167
    SECTION("passing malformed tokens create_user() results in a logged out user") {
70✔
168
        store->create_user(user_id, refresh_token, "not a token", device_id);
4✔
169
        auto data = store->get_user(user_id);
4✔
170
        REQUIRE(data);
4!
171
        REQUIRE(data->access_token.token == "");
4!
172
        REQUIRE(data->refresh_token.token == "");
4!
173
        REQUIRE(data->device_id == device_id);
4!
174
    }
4✔
175

34✔
176
    SECTION("create_user() marks the new user as the current user if it was created") {
70✔
177
        CHECK(store->get_current_user() == "");
4!
178
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
179
        CHECK(store->get_current_user() == user_id);
4!
180
        store->create_user("user 2", refresh_token, access_token, device_id);
4✔
181
        CHECK(store->get_current_user() == "user 2");
4!
182
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
183
        CHECK(store->get_current_user() == "user 2");
4!
184
    }
4✔
185

34✔
186
    SECTION("create_user() only updates the given fields and leaves the rest unchanged") {
70✔
187
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
188
        auto data = store->get_user(user_id);
4✔
189
        REQUIRE(data);
4!
190
        data->profile = bson::BsonDocument{{"name", "user's name"}, {"email", "user's email"}};
4✔
191
        data->identities = {{"identity", "provider"}};
4✔
192
        store->update_user(user_id, *data);
4✔
193

2✔
194
        const auto access_token_2 = encode_fake_jwt("access_token_2", 123, 456);
4✔
195
        const auto refresh_token_2 = encode_fake_jwt("refresh_token_2", 123, 456);
4✔
196
        store->create_user(user_id, refresh_token_2, access_token_2, "device id 2");
4✔
197

2✔
198
        auto data2 = store->get_user(user_id);
4✔
199
        REQUIRE(data2);
4!
200
        CHECK(data2->access_token.token == access_token_2);
4!
201
        CHECK(data2->refresh_token.token == refresh_token_2);
4!
202
        CHECK(data2->legacy_identities.empty());
4!
203
        CHECK(data2->device_id == "device id 2");
4!
204
        CHECK(data2->identities == data->identities);
4!
205
        CHECK(data2->profile.data() == data->profile.data());
4!
206
    }
4✔
207

34✔
208
    SECTION("has_logged_in_user() is only true if user is present and valid") {
70✔
209
        CHECK_FALSE(store->has_logged_in_user(""));
4!
210
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
211

2✔
212
        store->create_user(user_id, refresh_token, "malformed token", device_id);
4✔
213
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
214
        store->create_user(user_id, refresh_token, "", device_id);
4✔
215
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
216
        store->create_user(user_id, "malformed token", access_token, device_id);
4✔
217
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
218
        store->create_user(user_id, "", access_token, device_id);
4✔
219
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
220

2✔
221
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
222
        store->log_out(user_id, SyncUser::State::LoggedOut);
4✔
223
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
224

2✔
225
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
226
        store->log_out(user_id, SyncUser::State::Removed);
4✔
227
        CHECK_FALSE(store->has_logged_in_user(user_id));
4!
228

2✔
229
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
230
        CHECK(store->has_logged_in_user(user_id));
4!
231
        CHECK_FALSE(store->has_logged_in_user(""));
4!
232
        CHECK_FALSE(store->has_logged_in_user("different user"));
4!
233
    }
4✔
234

34✔
235
    SECTION("get_all_users() returns all non-removed users") {
70✔
236
        store->create_user("user 1", refresh_token, access_token, device_id);
4✔
237
        store->create_user("user 2", refresh_token, access_token, device_id);
4✔
238
        store->create_user("user 3", refresh_token, access_token, device_id);
4✔
239
        store->create_user("user 4", refresh_token, access_token, device_id);
4✔
240

2✔
241
        CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3", "user 4"});
4!
242

2✔
243
        store->log_out("user 2", SyncUser::State::LoggedOut);
4✔
244
        store->delete_user(file_manager, "user 4");
4✔
245

2✔
246
        CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"});
4!
247
        CHECK(store->has_logged_in_user("user 1"));
4!
248
        CHECK(!store->has_logged_in_user("user 2"));
4!
249
        CHECK(store->has_logged_in_user("user 3"));
4!
250
        CHECK(!store->has_logged_in_user("user 4"));
4!
251

2✔
252
        store->create_user("user 1", "", access_token, device_id);
4✔
253
        CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"});
4!
254
        CHECK(!store->has_logged_in_user("user 1"));
4!
255
        CHECK(!store->has_logged_in_user("user 2"));
4!
256
        CHECK(store->has_logged_in_user("user 3"));
4!
257
        CHECK(!store->has_logged_in_user("user 4"));
4!
258

2✔
259
        store->create_user("user 3", refresh_token, "", device_id);
4✔
260
        CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"});
4!
261
        CHECK(!store->has_logged_in_user("user 1"));
4!
262
        CHECK(!store->has_logged_in_user("user 2"));
4!
263
        CHECK(!store->has_logged_in_user("user 3"));
4!
264
        CHECK(!store->has_logged_in_user("user 4"));
4!
265

2✔
266
        store->delete_user(file_manager, "user 1");
4✔
267
        store->delete_user(file_manager, "user 2");
4✔
268
        store->delete_user(file_manager, "user 3");
4✔
269
        store->delete_user(file_manager, "user 4");
4✔
270
        CHECK(store->get_all_users().empty());
4!
271
        CHECK(!store->has_logged_in_user("user 1"));
4!
272
        CHECK(!store->has_logged_in_user("user 2"));
4!
273
        CHECK(!store->has_logged_in_user("user 3"));
4!
274
        CHECK(!store->has_logged_in_user("user 4"));
4!
275
    }
4✔
276

34✔
277
    SECTION("set_current_user() sets to the requested user") {
70✔
278
        CHECK(store->get_current_user() == "");
4!
279
        store->create_user("user 1", refresh_token, access_token, device_id);
4✔
280
        CHECK(store->get_current_user() == "user 1");
4!
281
        store->create_user("user 2", refresh_token, access_token, device_id);
4✔
282
        CHECK(store->get_current_user() == "user 2");
4!
283

2✔
284
        store->set_current_user("");
4✔
285
        CHECK(store->get_current_user() == "user 1");
4!
286
        store->set_current_user("user 2");
4✔
287
        CHECK(store->get_current_user() == "user 2");
4!
288
        store->set_current_user("user 1");
4✔
289
        CHECK(store->get_current_user() == "user 1");
4!
290
    }
4✔
291

34✔
292
    SECTION("current user falls back to the first valid one if current is invalid") {
70✔
293
        store->create_user("user 1", refresh_token, access_token, device_id);
4✔
294
        store->create_user("user 2", refresh_token, access_token, device_id);
4✔
295
        store->create_user("user 3", refresh_token, access_token, device_id);
4✔
296

2✔
297
        auto data = store->get_user("user 3");
4✔
298
        data->access_token.token.clear();
4✔
299
        data->refresh_token.token.clear();
4✔
300
        store->update_user("user 3", *data);
4✔
301
        CHECK(store->get_current_user() == "user 1");
4!
302
        store->update_user("user 1", *data);
4✔
303
        CHECK(store->get_current_user() == "user 2");
4!
304

2✔
305
        store->set_current_user("not a user");
4✔
306
        CHECK(store->get_current_user() == "user 2");
4!
307
        store->set_current_user("");
4✔
308
        CHECK(store->get_current_user() == "user 2");
4!
309
    }
4✔
310

34✔
311
    SECTION("log_out() updates the user state without deleting anything") {
70✔
312
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
313
        auto path = File::resolve("file 1", base_path);
4✔
314
        File(path, File::mode_Write);
4✔
315
        CHECK(File::exists(path));
4!
316
        store->add_realm_path(user_id, path);
4✔
317
        store->add_realm_path(user_id, "invalid path");
4✔
318
        store->log_out(user_id, SyncUser::State::Removed);
4✔
319
        CHECK(File::exists(path));
4!
320
    }
4✔
321

34✔
322
    SECTION("delete_user() deletes the files recorded with add_realm_file_path()") {
70✔
323
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
324
        auto path = File::resolve("file 1", base_path);
4✔
325
        File(path, File::mode_Write);
4✔
326
        CHECK(File::exists(path));
4!
327
        store->add_realm_path(user_id, path);
4✔
328
        store->add_realm_path(user_id, "invalid path");
4✔
329
        store->delete_user(file_manager, user_id);
4✔
330
        CHECK_FALSE(File::exists(path));
4!
331
    }
4✔
332

34✔
333
    SECTION("update_user() does not set legacy identities") {
70✔
334
        store->create_user(user_id, refresh_token, access_token, device_id);
4✔
335
        auto data = store->get_user(user_id);
4✔
336
        data->legacy_identities.push_back("legacy uuid");
4✔
337
        store->update_user(user_id, *data);
4✔
338
        data = store->get_user(user_id);
4✔
339
        REQUIRE(data->legacy_identities.empty());
4!
340
    }
4✔
341

34✔
342
    SECTION("immediately run nonexistent action") {
70✔
343
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, "invalid"));
4!
344
    }
4✔
345

34✔
346
    SECTION("immediately run DeleteRealm action") {
70✔
347
        auto path = util::make_temp_file("delete-realm-action");
4✔
348
        store->create_file_action(SyncFileAction::DeleteRealm, path, {});
4✔
349
        CHECK(File::exists(path));
4!
350
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
351
        CHECK_FALSE(File::exists(path));
4!
352
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
353
    }
4✔
354

34✔
355
    SECTION("immediately run BackUpThenDeleteRealm action") {
70✔
356
        auto path = util::make_temp_file("delete-realm-action");
4✔
357
        auto backup_path = util::make_temp_file("backup-path");
4✔
358
        File::remove(backup_path);
4✔
359
        store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path);
4✔
360
        CHECK(File::exists(path));
4!
361
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
362
        CHECK_FALSE(File::exists(path));
4!
363
        CHECK(File::exists(backup_path));
4!
364
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
365
    }
4✔
366

34✔
367
    SECTION("file actions replace existing ones for the same path") {
70✔
368
        auto path = util::make_temp_file("delete-realm-action");
4✔
369
        auto backup_path = util::make_temp_file("backup-path");
4✔
370
        store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path);
4✔
371
        store->create_file_action(SyncFileAction::DeleteRealm, path, {});
4✔
372
        CHECK(File::exists(path));
4!
373
        // Would return false if it tried to perform a backup
2✔
374
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
375
        CHECK_FALSE(File::exists(path));
4!
376
    }
4✔
377

34✔
378
    SECTION("failed backup action is preserved") {
70✔
379
        auto path = util::make_temp_file("delete-realm-action");
4✔
380
        auto backup_path = util::make_temp_file("backup-path");
4✔
381
        store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path);
4✔
382
        CHECK(File::exists(path));
4!
383
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
384
        File::remove(backup_path);
4✔
385
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
386
        CHECK_FALSE(File::exists(path));
4!
387
        CHECK(File::exists(backup_path));
4!
388
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
389
    }
4✔
390

34✔
391
#if REALM_PLATFORM_APPLE
36✔
392
    SECTION("failed delete after backup succeeds turns into a delete action") {
36✔
393
        auto path = util::make_temp_file("delete-realm-action");
2✔
394
        auto backup_path = util::make_temp_file("backup-path");
2✔
395
        File::remove(backup_path);
2✔
396
        store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path);
2✔
397
        CHECK(File::exists(path));
2!
398

399
        REQUIRE(chflags(path.c_str(), UF_IMMUTABLE) == 0);
2!
400
        // Returns false because it did something, but did not complete
401
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
2!
402
        CHECK(File::exists(path));
2!
403
        CHECK(File::exists(backup_path));
2!
404

405
        // Should try again to remove the original file, but not perform another backup
406
        REQUIRE(chflags(path.c_str(), 0) == 0);
2!
407
        REQUIRE(chflags(backup_path.c_str(), 0) == 0);
2!
408
        File::remove(backup_path);
2✔
409
        CHECK(store->immediately_run_file_actions(file_manager, path));
2!
410
        CHECK_FALSE(File::exists(path));
2!
411
        CHECK_FALSE(File::exists(backup_path));
2!
412
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
2!
413
    }
2✔
414
#endif
36✔
415

34✔
416
    SECTION("file action on deleted file is considered successful") {
70✔
417
        auto path = util::make_temp_file("delete-realm-action");
4✔
418
        File::remove(path);
4✔
419

2✔
420
        store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, path);
4✔
421
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
422
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
423

2✔
424
        store->create_file_action(SyncFileAction::DeleteRealm, path, {});
4✔
425
        CHECK(store->immediately_run_file_actions(file_manager, path));
4!
426
        CHECK_FALSE(store->immediately_run_file_actions(file_manager, path));
4!
427
    }
4✔
428
}
70✔
429

430
TEST_CASE("app metadata: in memory", "[sync][metadata]") {
2✔
431
    test_util::TestDirGuard test_dir(base_path);
2✔
432
    AppConfig config;
2✔
433
    config.app_id = app_id;
2✔
434
    config.metadata_mode = AppConfig::MetadataMode::InMemory;
2✔
435
    config.base_file_path = base_path;
2✔
436
    SyncFileManager file_manager(config);
2✔
437

1✔
438
    SECTION("does not persist users between instances") {
2✔
439
        {
2✔
440
            auto store = create_metadata_store(config, file_manager);
2✔
441
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
442
        }
2✔
443
        {
2✔
444
            auto store = create_metadata_store(config, file_manager);
2✔
445
            CHECK_FALSE(store->has_logged_in_user(user_id));
2!
446
        }
2✔
447
    }
2✔
448
}
2✔
449

450
TEST_CASE("app metadata: persisted", "[sync][metadata]") {
15✔
451
    test_util::TestDirGuard test_dir(base_path);
15✔
452

7✔
453
    AppConfig config;
15✔
454
    config.app_id = app_id;
15✔
455
    config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
15✔
456
    config.base_file_path = base_path;
15✔
457
    SyncFileManager file_manager(config);
15✔
458

7✔
459
    SECTION("persists users between instances") {
15✔
460
        {
2✔
461
            auto store = create_metadata_store(config, file_manager);
2✔
462
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
463
        }
2✔
464
        {
2✔
465
            auto store = create_metadata_store(config, file_manager);
2✔
466
            CHECK(store->has_logged_in_user(user_id));
2!
467
            store->log_out(user_id, SyncUser::State::LoggedOut);
2✔
468
        }
2✔
469
        {
2✔
470
            auto store = create_metadata_store(config, file_manager);
2✔
471
            CHECK_FALSE(store->has_logged_in_user(user_id));
2!
472
            CHECK(store->get_all_users() == Strings{user_id});
2!
473
        }
2✔
474
    }
2✔
475

7✔
476
    SECTION("can read legacy identities if present") {
15✔
477
        auto store = create_metadata_store(config, file_manager);
2✔
478
        store->create_user(user_id, refresh_token, access_token, device_id);
2✔
479

1✔
480
        auto data = store->get_user(user_id);
2✔
481
        CHECK(data->legacy_identities.empty());
2!
482

1✔
483
        {
2✔
484
            // Add some legacy uuids by modifying the underlying realm directly
1✔
485
            auto realm = get_metadata_realm();
2✔
486
            auto table = realm->read_group().get_table("class_UserMetadata");
2✔
487
            REQUIRE(table);
2!
488
            REQUIRE(table->size() == 1);
2!
489
            auto list = table->begin()->get_list<String>("legacy_uuids");
2✔
490
            realm->begin_transaction();
2✔
491
            list.add("uuid 1");
2✔
492
            list.add("uuid 2");
2✔
493
            realm->commit_transaction();
2✔
494
        }
2✔
495

1✔
496
        data = store->get_user(user_id);
2✔
497
        CHECK(data->legacy_identities == std::vector<std::string>{"uuid 1", "uuid 2"});
2!
498
    }
2✔
499

7✔
500
    SECTION("runs file actions on creation") {
15✔
501
        auto path = util::make_temp_file("file_to_delete");
2✔
502
        auto nonexistent = util::make_temp_file("nonexistent");
2✔
503
        File::remove(nonexistent);
2✔
504

1✔
505
        {
2✔
506
            auto store = create_metadata_store(config, file_manager);
2✔
507
            store->create_file_action(SyncFileAction::DeleteRealm, path, "");
2✔
508
            store->create_file_action(SyncFileAction::DeleteRealm, nonexistent, "");
2✔
509
        }
2✔
510

1✔
511
        create_metadata_store(config, file_manager);
2✔
512
        REQUIRE_FALSE(File::exists(path));
2!
513
        REQUIRE_FALSE(File::exists(nonexistent));
2!
514

1✔
515
        // Check the underlying realm to verify both file actions are gone
1✔
516
        auto realm = get_metadata_realm();
2✔
517
        CHECK(realm->read_group().get_table("class_FileActionMetadata")->is_empty());
2!
518
    }
2✔
519

7✔
520
    SECTION("deletes data for removed users on creation") {
15✔
521
        {
2✔
522
            auto store = create_metadata_store(config, file_manager);
2✔
523
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
524
            store->log_out(user_id, SyncUser::State::Removed);
2✔
525
        }
2✔
526
        {
2✔
527
            auto store = create_metadata_store(config, file_manager);
2✔
528
            CHECK(store->get_all_users().empty());
2!
529
        }
2✔
530
        // Check the underlying realm as removed users aren't exposed in the API
1✔
531
        auto realm = get_metadata_realm();
2✔
532
        CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty());
2!
533
    }
2✔
534

7✔
535
    SECTION("deletes realm files for removed users on creation") {
15✔
536
        auto path = util::make_temp_file("file_to_delete");
2✔
537
        auto nonexistent = util::make_temp_file("nonexistent");
2✔
538
        REQUIRE(File::exists(path));
2!
539
        File::remove(nonexistent);
2✔
540

1✔
541
        {
2✔
542
            auto store = create_metadata_store(config, file_manager);
2✔
543
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
544
            store->add_realm_path(user_id, nonexistent);
2✔
545
            store->add_realm_path(user_id, path);
2✔
546
            store->log_out(user_id, SyncUser::State::Removed);
2✔
547
        }
2✔
548

1✔
549
        create_metadata_store(config, file_manager);
2✔
550
        REQUIRE_FALSE(File::exists(path));
2!
551
        REQUIRE_FALSE(File::exists(nonexistent));
2!
552
    }
2✔
553

7✔
554
#if REALM_PLATFORM_APPLE
8✔
555
    SECTION("continues tracking files to delete if deletion fails") {
8✔
556
        auto path = util::make_temp_file("file_to_delete");
1✔
557
        REQUIRE(File::exists(path));
1!
558

559
        {
1✔
560
            auto store = create_metadata_store(config, file_manager);
1✔
561
            store->create_user(user_id, refresh_token, access_token, device_id);
1✔
562
            store->add_realm_path(user_id, path);
1✔
563
            store->log_out(user_id, SyncUser::State::Removed);
1✔
564
        }
1✔
565

566
        REQUIRE(chflags(path.c_str(), UF_IMMUTABLE) == 0);
1!
567
        create_metadata_store(config, file_manager);
1✔
568
        REQUIRE(File::exists(path));
1!
569
        REQUIRE(chflags(path.c_str(), 0) == 0);
1!
570
        create_metadata_store(config, file_manager);
1✔
571
        REQUIRE_FALSE(File::exists(path));
1!
572
    }
1✔
573
#endif
8✔
574

7✔
575
    SECTION("stops tracking files if it no longer exists") {
15✔
576
        auto path = util::make_temp_file("nonexistent");
2✔
577
        {
2✔
578
            auto store = create_metadata_store(config, file_manager);
2✔
579
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
580
            store->add_realm_path(user_id, path);
2✔
581
            store->log_out(user_id, SyncUser::State::Removed);
2✔
582
        }
2✔
583

1✔
584
        File::remove(path);
2✔
585
        create_metadata_store(config, file_manager);
2✔
586
        auto realm = get_metadata_realm();
2✔
587
        CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty());
2!
588
    }
2✔
589

7✔
590
    SECTION("deletes legacy untracked files") {
15✔
591
        {
2✔
592
            auto store = create_metadata_store(config, file_manager);
2✔
593
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
594
            store->log_out(user_id, SyncUser::State::Removed);
2✔
595
        }
2✔
596

1✔
597
        // Create some files in the user's directory without tracking them
1✔
598
        auto path_1 = file_manager.realm_file_path(user_id, {}, "file 1", "partition 1");
2✔
599
        auto path_2 = file_manager.realm_file_path(user_id, {}, "file 2", "partition 2");
2✔
600
        File{path_1, File::mode_Write};
2✔
601
        File{path_2, File::mode_Write};
2✔
602

1✔
603
        // Files should be deleted on next start since the user has been removed
1✔
604
        create_metadata_store(config, file_manager);
2✔
605
        CHECK_FALSE(File::exists(path_1));
2!
606
        CHECK_FALSE(File::exists(path_2));
2!
607
    }
2✔
608
}
15✔
609

610
TEST_CASE("app metadata: encryption", "[sync][metadata]") {
5✔
611
    test_util::TestDirGuard test_dir(base_path);
5✔
612

3✔
613
    AppConfig config;
5✔
614
    config.app_id = app_id;
5✔
615
    config.metadata_mode = AppConfig::MetadataMode::Encryption;
5✔
616
    config.custom_encryption_key = make_test_encryption_key(10);
5✔
617
    config.base_file_path = base_path;
5✔
618
    SyncFileManager file_manager(config);
5✔
619

3✔
620
    // Verify that the Realm is actually encrypted with the expected key
3✔
621
    auto open_realm_with_key = [](auto& key) {
10✔
622
        RealmConfig realm_config;
10✔
623
        realm_config.automatic_change_notifications = false;
10✔
624
        realm_config.path = metadata_path;
10✔
625
        // sanity check that using the wrong key throws, as otherwise we'd pass
5✔
626
        // if we were checking the wrong path
5✔
627
        realm_config.encryption_key = make_test_encryption_key(0);
10✔
628
        CHECK_THROWS(Realm::get_shared_realm(realm_config));
10✔
629

5✔
630
        if (key) {
10✔
631
            realm_config.encryption_key = *key;
8✔
632
        }
8✔
633
        else {
2✔
634
            realm_config.encryption_key.clear();
2✔
635
        }
2✔
636
        CHECK_NOTHROW(Realm::get_shared_realm(realm_config));
10✔
637
    };
10✔
638

3✔
639
    SECTION("can open and reopen with an explicit key") {
5✔
640
        {
2✔
641
            auto store = create_metadata_store(config, file_manager);
2✔
642
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
643
        }
2✔
644
        {
2✔
645
            auto store = create_metadata_store(config, file_manager);
2✔
646
            CHECK(store->has_logged_in_user(user_id));
2!
647
        }
2✔
648
        open_realm_with_key(config.custom_encryption_key);
2✔
649
    }
2✔
650

3✔
651
    SECTION("reopening with a different key deletes the existing data") {
5✔
652
        {
2✔
653
            auto store = create_metadata_store(config, file_manager);
2✔
654
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
655
        }
2✔
656
        open_realm_with_key(config.custom_encryption_key);
2✔
657

1✔
658
        // Change to new encryption key
1✔
659
        {
2✔
660
            config.custom_encryption_key = make_test_encryption_key(11);
2✔
661
            auto store = create_metadata_store(config, file_manager);
2✔
662
            CHECK_FALSE(store->has_logged_in_user(user_id));
2!
663
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
664
        }
2✔
665
        open_realm_with_key(config.custom_encryption_key);
2✔
666

1✔
667
        // Change to unencrypted
1✔
668
        {
2✔
669
            config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
2✔
670
            config.custom_encryption_key.reset();
2✔
671
            auto store = create_metadata_store(config, file_manager);
2✔
672
            CHECK_FALSE(store->has_logged_in_user(user_id));
2!
673
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
674
        }
2✔
675
        open_realm_with_key(config.custom_encryption_key);
2✔
676

1✔
677
        // Change back to encrypted
1✔
678
        {
2✔
679
            config.metadata_mode = AppConfig::MetadataMode::Encryption;
2✔
680
            config.custom_encryption_key = make_test_encryption_key(12);
2✔
681
            auto store = create_metadata_store(config, file_manager);
2✔
682
            CHECK_FALSE(store->has_logged_in_user(user_id));
2!
683
            store->create_user(user_id, refresh_token, access_token, device_id);
2✔
684
        }
2✔
685
        open_realm_with_key(config.custom_encryption_key);
2✔
686
    }
2✔
687

3✔
688
#if REALM_PLATFORM_APPLE
2✔
689
    if (!can_access_keychain()) {
2✔
690
        return;
2✔
691
    }
2✔
692
    auto delete_key = util::make_scope_exit([&]() noexcept {
693
        keychain::delete_metadata_realm_encryption_key(config.app_id, config.security_access_group);
694
    });
695

696
    SECTION("encryption key is automatically generated and stored for new files") {
697
        config.custom_encryption_key.reset();
698
        {
699
            auto store = create_metadata_store(config, file_manager);
700
            store->create_user(user_id, refresh_token, access_token, device_id);
701
        }
702
        auto key = keychain::get_existing_metadata_realm_key(config.app_id, config.security_access_group);
703
        REQUIRE(key);
×
704
        {
705
            auto store = create_metadata_store(config, file_manager);
706
            CHECK(store->has_logged_in_user(user_id));
×
707
        }
708
        open_realm_with_key(key);
709
    }
710

711
    SECTION("existing unencrypted files are left unencrypted") {
712
        config.custom_encryption_key.reset();
713
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
714
        {
715
            auto store = create_metadata_store(config, file_manager);
716
            store->create_user(user_id, refresh_token, access_token, device_id);
717
        }
718

719
        config.metadata_mode = AppConfig::MetadataMode::Encryption;
720
        {
721
            auto store = create_metadata_store(config, file_manager);
722
            CHECK(store->has_logged_in_user(user_id));
×
723
        }
724
        open_realm_with_key(config.custom_encryption_key);
725
    }
726
#else  // REALM_PLATFORM_APPLE
727
    SECTION("requires an explicit encryption key") {
3✔
728
        config.custom_encryption_key.reset();
1✔
729
        REQUIRE_EXCEPTION(create_metadata_store(config, file_manager), InvalidArgument,
1✔
730
                          "Metadata Realm encryption was specified, but no encryption key was provided.");
1✔
731
    }
1✔
732
#endif // REALM_PLATFORM_APPLE
3✔
733
}
3✔
734

735
#ifndef SWIFT_PACKAGE // The SPM build currently doesn't copy resource files
736
TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") {
6✔
737
    test_util::TestDirGuard test_dir(base_path);
6✔
738

3✔
739
    util::make_dir_recursive(File::parent_dir(metadata_path));
6✔
740

3✔
741
    const std::string provider_type = "https://realm.example.org";
6✔
742
    const auto identity = "metadata migration test";
6✔
743
    const std::string sample_token = encode_fake_jwt("metadata migration token", 456, 123);
6✔
744

3✔
745
    const auto access_token_1 = encode_fake_jwt("access token 1", 456, 123);
6✔
746
    const auto access_token_2 = encode_fake_jwt("access token 2", 456, 124);
6✔
747
    const auto refresh_token_1 = encode_fake_jwt("refresh token 1", 456, 123);
6✔
748
    const auto refresh_token_2 = encode_fake_jwt("refresh token 2", 456, 124);
6✔
749

3✔
750
    AppConfig config;
6✔
751
    config.app_id = app_id;
6✔
752
    config.base_file_path = base_path;
6✔
753
    config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
6✔
754
    SyncFileManager file_manager(config);
6✔
755

3✔
756

3✔
757
    // change to true to create a test file for the current schema version
3✔
758
    // this will only work on unix-like systems
3✔
759
    if ((false)) {
6✔
760
#if false   // The code to generate the v4 and v5 Realms
761
        { // Create a metadata Realm with a test user
762
            SyncMetadataManager manager(metadata_path, false);
763
            auto user_metadata = manager.get_or_make_user_metadata(identity, provider_type);
764
            user_metadata->set_access_token(sample_token);
765
        }
766
#elif false // The code to generate the v6 Realm
767
        // Code to generate the v6 metadata Realm used to test the 6 -> 7 migration
768
        {
769
            using State = SyncUser::State;
770
            SyncMetadataManager manager(metadata_path, false);
771

772
            auto user = manager.get_or_make_user_metadata("removed user", "");
773
            user->set_state(State::Removed);
774

775
            auto make_user_pair = [&](const char* name, State state1, State state2, const std::string& token_1,
776
                                      const std::string& token_2) {
777
                auto user = manager.get_or_make_user_metadata(name, "a");
778
                user->set_state_and_tokens(state1, token_1, refresh_token_1);
779
                user->set_identities({{"identity 1", "a"}, {"shared identity", "shared"}});
780
                user->add_realm_file_path("file 1");
781
                user->add_realm_file_path("file 2");
782
                
783
                user = manager.get_or_make_user_metadata(name, "b");
784
                user->set_state_and_tokens(state2, token_2, refresh_token_2);
785
                user->set_identities({{"identity 2", "b"}, {"shared identity", "shared"}});
786
                user->add_realm_file_path("file 2");
787
                user->add_realm_file_path("file 3");
788
            };
789

790
            make_user_pair("first logged in, second logged out", State::LoggedIn, State::LoggedOut, access_token_1,
791
                           access_token_2);
792
            make_user_pair("first logged in, second removed", State::LoggedIn, State::Removed, access_token_1,
793
                           access_token_2);
794
            make_user_pair("second logged in, first logged out", State::LoggedOut, State::LoggedIn, access_token_1,
795
                           access_token_2);
796
            make_user_pair("second logged in, first removed", State::Removed, State::LoggedIn, access_token_1,
797
                           access_token_2);
798
            make_user_pair("both logged in, first newer", State::LoggedIn, State::LoggedIn, access_token_2,
799
                           access_token_1);
800
            make_user_pair("both logged in, second newer", State::LoggedIn, State::LoggedIn, access_token_1,
801
                           access_token_2);
802
        }
803

804
        // Replace the randomly generated UUIDs with deterministic values
805
        {
806
            Realm::Config config;
807
            config.path = metadata_path;
808
            auto realm = Realm::get_shared_realm(config);
809
            realm->begin_transaction();
810
            auto& group = realm->read_group();
811
            auto table = group.get_table("class_UserMetadata");
812
            auto col = table->get_column_key("local_uuid");
813
            size_t i = 0;
814
            for (auto& obj : *table) {
815
                obj.set(col, util::to_string(i++));
816
            }
817
            realm->commit_transaction();
818
        }
819
#else
UNCOV
820
        { // Create a metadata Realm with a test user
×
UNCOV
821
            auto store = create_metadata_store(config, file_manager);
×
UNCOV
822
            store->create_user(identity, sample_token, sample_token, "device id");
×
UNCOV
823
        }
×
UNCOV
824
#endif
×
825

826
        // Open the metadata Realm directly and grab the schema version from it
UNCOV
827
        auto realm = get_metadata_realm();
×
UNCOV
828
        realm->read_group();
×
UNCOV
829
        auto schema_version = realm->schema_version();
×
830

831
        // Take the path of this file, remove everything after the "test" directory,
832
        // then append the output filename
UNCOV
833
        std::string out_path = __FILE__;
×
UNCOV
834
        auto suffix = out_path.find("sync/metadata.cpp");
×
UNCOV
835
        REQUIRE(suffix != out_path.npos);
×
UNCOV
836
        out_path.resize(suffix);
×
UNCOV
837
        out_path.append(util::format("sync-metadata-v%1.realm", schema_version));
×
838

839
        // Write a compacted copy of the metadata realm to the test directory
UNCOV
840
        Realm::Config out_config;
×
UNCOV
841
        out_config.path = out_path;
×
UNCOV
842
        realm->convert(out_config);
×
843

UNCOV
844
        std::cout << "Wrote metadata realm to: " << out_path << "\n";
×
UNCOV
845
        return;
×
846
    }
6✔
847

3✔
848
    SECTION("open schema version 4") {
6✔
849
        File::copy(test_util::get_test_resource_path() + "sync-metadata-v4.realm", metadata_path);
2✔
850
        auto store = create_metadata_store(config, file_manager);
2✔
851
        auto user_metadata = store->get_user(identity);
2✔
852
        REQUIRE(user_metadata->access_token.token == sample_token);
2!
853
    }
2✔
854

3✔
855
    SECTION("open schema version 5") {
6✔
856
        File::copy(test_util::get_test_resource_path() + "sync-metadata-v5.realm", metadata_path);
2✔
857
        auto store = create_metadata_store(config, file_manager);
2✔
858
        auto user_metadata = store->get_user(identity);
2✔
859
        REQUIRE(user_metadata->access_token.token == sample_token);
2!
860
    }
2✔
861

3✔
862
    SECTION("open schema version 6") {
6✔
863
        File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path);
2✔
864
        auto store = create_metadata_store(config, file_manager);
2✔
865

1✔
866
        UserIdentity id_1{"identity 1", "a"};
2✔
867
        UserIdentity id_2{"identity 2", "b"};
2✔
868
        UserIdentity id_shared{"shared identity", "shared"};
2✔
869
        const std::vector<UserIdentity> all_ids = {id_1, id_shared, id_2};
2✔
870
        const std::vector<std::string> realm_files = {"file 1", "file 2", "file 3"};
2✔
871

1✔
872
        auto check_user = [&](const char* user_id, const std::string& access_token, const std::string& refresh_token,
2✔
873
                              const std::vector<std::string>& uuids) {
12✔
874
            auto user = store->get_user(user_id);
12✔
875
            CAPTURE(user_id);
12✔
876
            CHECK(user->access_token.token == access_token);
12!
877
            CHECK(user->refresh_token.token == refresh_token);
12!
878
            CHECK(user->legacy_identities == uuids);
12!
879
            CHECK(user->identities == all_ids);
12!
880
        };
12✔
881

1✔
882
        REQUIRE_FALSE(store->has_logged_in_user("removed user"));
2!
883
        check_user("first logged in, second logged out", access_token_1, refresh_token_1, {"1", "2"});
2✔
884
        check_user("first logged in, second removed", access_token_1, refresh_token_1, {"3", "4"});
2✔
885
        check_user("second logged in, first logged out", access_token_2, refresh_token_2, {"5", "6"});
2✔
886
        check_user("second logged in, first removed", access_token_2, refresh_token_2, {"7", "8"});
2✔
887
        check_user("both logged in, first newer", access_token_2, refresh_token_1, {"9", "10"});
2✔
888
        check_user("both logged in, second newer", access_token_2, refresh_token_2, {"11", "12"});
2✔
889
    }
2✔
890
}
6✔
891
#endif // SWIFT_PACKAGE
892

893
#if REALM_PLATFORM_APPLE
894
TEST_CASE("keychain", "[sync][metadata]") {
1✔
895
    if (!can_access_keychain()) {
1✔
896
        return;
1✔
897
    }
1✔
898
    auto delete_key = util::make_scope_exit([=]() noexcept {
899
        keychain::delete_metadata_realm_encryption_key(app_id, access_group);
900
        keychain::delete_metadata_realm_encryption_key("app id 1", access_group);
901
        keychain::delete_metadata_realm_encryption_key("app id 2", access_group);
902
    });
903

904
    SECTION("create_new_metadata_realm_key() creates a new key if none exists") {
905
        auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group);
906
        REQUIRE(key_1);
×
907
        keychain::delete_metadata_realm_encryption_key(app_id, access_group);
908
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
909
        REQUIRE(key_2);
×
910
        REQUIRE(key_1 != key_2);
×
911
    }
912

913
    SECTION("create_new_metadata_realm_key() returns the existing one if inserting fails") {
914
        auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group);
915
        REQUIRE(key_1);
×
916
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
917
        REQUIRE(key_2);
×
918
        REQUIRE(key_1 == key_2);
×
919
    }
920

921
    SECTION("get_existing_metadata_realm_key() returns the key from create_new_metadata_realm_key()") {
922
        auto key_1 = keychain::get_existing_metadata_realm_key(app_id, access_group);
923
        REQUIRE_FALSE(key_1);
×
924
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
925
        REQUIRE(key_2);
×
926
        auto key_3 = keychain::get_existing_metadata_realm_key(app_id, access_group);
927
        REQUIRE(key_3);
×
928
        REQUIRE(key_2 == key_3);
×
929
    }
930

931
    SECTION("keys are scoped to app ids") {
932
        auto key_1 = keychain::create_new_metadata_realm_key("app id 1", access_group);
933
        REQUIRE(key_1);
×
934
        auto key_2 = keychain::create_new_metadata_realm_key("app id 2", access_group);
935
        REQUIRE(key_2);
×
936
        REQUIRE(key_1 != key_2);
×
937
    }
938

939
    SECTION("legacy key migration") {
940
        auto key = generate_key();
941
        const auto legacy_account = CFSTR("metadata");
942
        const auto service_name = CFSTR("io.realm.sync.keychain");
943
        const auto bundle_id = CFBundleGetIdentifier(CFBundleGetMainBundle());
944
        // Could be either ObjectStoreTests or CombinedTests but must be set
945
        REQUIRE(bundle_id);
×
946
        const auto bundle_service =
947
            adoptCF(CFStringCreateWithFormat(nullptr, nullptr, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id));
948

949
        enum class Location { Original, Bundle, BundleAndAppId };
950
        auto location = GENERATE(Location::Original, Location::Bundle, Location::BundleAndAppId);
951
        CAPTURE(location);
952
        CFStringRef account, service;
953
        switch (location) {
×
954
            case Location::Original:
×
955
                account = legacy_account;
956
                service = service_name;
957
                break;
958
            case Location::Bundle:
×
959
                account = legacy_account;
960
                service = bundle_service.get();
961
                break;
962
            case Location::BundleAndAppId:
×
963
                account = CFSTR("app id");
964
                service = bundle_service.get();
965
                break;
966
        }
967

968
        set_key(key, account, service);
969
        auto key_2 = keychain::get_existing_metadata_realm_key(app_id, {});
970
        REQUIRE(key_2 == key);
×
971

972
        // Key should have been copied to the preferred location
973
        REQUIRE(get_key(CFSTR("app id"), bundle_service.get(), key) == errSecSuccess);
×
974
        REQUIRE(key_2 == key);
×
975

976
        // Key should not have been deleted from the original location
977
        REQUIRE(get_key(account, service, key) == errSecSuccess);
×
978
        REQUIRE(key_2 == key);
×
979
    }
980
}
981
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc