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

realm / realm-core / 2267

27 Apr 2024 02:45AM UTC coverage: 90.734% (-0.02%) from 90.756%
2267

push

Evergreen

web-flow
Merge pull request #7639 from realm/release/14.6.0-again

merge release 14.6.0

101938 of 180236 branches covered (56.56%)

211 of 254 new or added lines in 5 files covered. (83.07%)

94 existing lines in 16 files now uncovered.

212406 of 234097 relevant lines covered (90.73%)

5753046.69 hits per line

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

95.67
/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
#if REALM_ENABLE_ENCRYPTION
63
constexpr const char* access_group = "";
64
bool can_access_keychain()
65
{
3✔
66
    static bool can_access_keychain = [] {
3✔
67
        bool can_access = keychain::create_new_metadata_realm_key(app_id, access_group) != none;
1✔
68
        if (can_access) {
1✔
69
            keychain::delete_metadata_realm_encryption_key(app_id, access_group);
70
        }
71
        else {
1✔
72
            std::cout << "Skipping keychain tests as the keychain is not accessible\n";
1✔
73
        }
1✔
74
        return can_access;
1✔
75
    }();
1✔
76
    return can_access_keychain;
3✔
77
}
3✔
78
#endif
79

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

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

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

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

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

144
using Strings = std::vector<std::string>;
145

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

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

156
    INFO(config.metadata_mode);
70✔
157

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

482
        auto data = store->get_user(user_id);
2✔
483
        CHECK(data->legacy_identities.empty());
2!
484

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

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

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

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

513
        create_metadata_store(config, file_manager);
2✔
514
        REQUIRE_FALSE(File::exists(path));
2!
515
        REQUIRE_FALSE(File::exists(nonexistent));
2!
516

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

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

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

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

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

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

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

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

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

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

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

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

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

612
#if REALM_ENABLE_ENCRYPTION
613
TEST_CASE("app metadata: encryption", "[sync][metadata]") {
5✔
614
    test_util::TestDirGuard test_dir(base_path);
5✔
615

616
    AppConfig config;
5✔
617
    config.app_id = app_id;
5✔
618
    config.metadata_mode = AppConfig::MetadataMode::Encryption;
5✔
619
    config.custom_encryption_key = make_test_encryption_key(10);
5✔
620
    config.base_file_path = base_path;
5✔
621
    SyncFileManager file_manager(config);
5✔
622

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

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

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

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

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

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

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

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

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

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

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

738
#endif
739

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

744
    util::make_dir_recursive(File::parent_dir(metadata_path));
6✔
745

746
    const std::string provider_type = "https://realm.example.org";
6✔
747
    const auto identity = "metadata migration test";
6✔
748
    const std::string sample_token = encode_fake_jwt("metadata migration token", 456, 123);
6✔
749

750
    const auto access_token_1 = encode_fake_jwt("access token 1", 456, 123);
6✔
751
    const auto access_token_2 = encode_fake_jwt("access token 2", 456, 124);
6✔
752
    const auto refresh_token_1 = encode_fake_jwt("refresh token 1", 456, 123);
6✔
753
    const auto refresh_token_2 = encode_fake_jwt("refresh token 2", 456, 124);
6✔
754

755
    AppConfig config;
6✔
756
    config.app_id = app_id;
6✔
757
    config.base_file_path = base_path;
6✔
758
    config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
6✔
759
    SyncFileManager file_manager(config);
6✔
760

761

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

777
            auto user = manager.get_or_make_user_metadata("removed user", "");
778
            user->set_state(State::Removed);
779

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

788
                user = manager.get_or_make_user_metadata(name, "b");
789
                user->set_state_and_tokens(state2, token_2, refresh_token_2);
790
                user->set_identities({{"identity 2", "b"}, {"shared identity", "shared"}});
791
                user->add_realm_file_path("file 2");
792
                user->add_realm_file_path("file 3");
793
            };
794

795
            make_user_pair("first logged in, second logged out", State::LoggedIn, State::LoggedOut, access_token_1,
796
                           access_token_2);
797
            make_user_pair("first logged in, second removed", State::LoggedIn, State::Removed, access_token_1,
798
                           access_token_2);
799
            make_user_pair("second logged in, first logged out", State::LoggedOut, State::LoggedIn, access_token_1,
800
                           access_token_2);
801
            make_user_pair("second logged in, first removed", State::Removed, State::LoggedIn, access_token_1,
802
                           access_token_2);
803
            make_user_pair("both logged in, first newer", State::LoggedIn, State::LoggedIn, access_token_2,
804
                           access_token_1);
805
            make_user_pair("both logged in, second newer", State::LoggedIn, State::LoggedIn, access_token_1,
806
                           access_token_2);
807
        }
808

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

831
        // Open the metadata Realm directly and grab the schema version from it
UNCOV
832
        auto realm = get_metadata_realm();
×
833
        realm->read_group();
×
834
        auto schema_version = realm->schema_version();
×
835

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

844
        // Write a compacted copy of the metadata realm to the test directory
845
        Realm::Config out_config;
×
846
        out_config.path = out_path;
×
UNCOV
847
        realm->convert(out_config);
×
848

UNCOV
849
        std::cout << "Wrote metadata realm to: " << out_path << "\n";
×
UNCOV
850
        return;
×
UNCOV
851
    }
×
852

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

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

867
    SECTION("open schema version 6") {
6✔
868
        File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path);
2✔
869
        auto store = create_metadata_store(config, file_manager);
2✔
870

871
        UserIdentity id_1{"identity 1", "a"};
2✔
872
        UserIdentity id_2{"identity 2", "b"};
2✔
873
        UserIdentity id_shared{"shared identity", "shared"};
2✔
874
        const std::vector<UserIdentity> all_ids = {id_1, id_shared, id_2};
2✔
875
        const std::vector<std::string> realm_files = {"file 1", "file 2", "file 3"};
2✔
876

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

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

898
#if REALM_PLATFORM_APPLE && REALM_ENABLE_ENCRYPTION
899
TEST_CASE("keychain", "[sync][metadata]") {
1✔
900
    if (!can_access_keychain()) {
1✔
901
        return;
1✔
902
    }
1✔
903
    auto delete_key = util::make_scope_exit([=]() noexcept {
904
        keychain::delete_metadata_realm_encryption_key(app_id, access_group);
905
        keychain::delete_metadata_realm_encryption_key("app id 1", access_group);
906
        keychain::delete_metadata_realm_encryption_key("app id 2", access_group);
907
    });
908

909
    SECTION("create_new_metadata_realm_key() creates a new key if none exists") {
910
        auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group);
911
        REQUIRE(key_1);
×
912
        keychain::delete_metadata_realm_encryption_key(app_id, access_group);
913
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
914
        REQUIRE(key_2);
×
915
        REQUIRE(key_1 != key_2);
×
916
    }
917

918
    SECTION("create_new_metadata_realm_key() returns the existing one if inserting fails") {
919
        auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group);
920
        REQUIRE(key_1);
×
921
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
922
        REQUIRE(key_2);
×
923
        REQUIRE(key_1 == key_2);
×
924
    }
925

926
    SECTION("get_existing_metadata_realm_key() returns the key from create_new_metadata_realm_key()") {
927
        auto key_1 = keychain::get_existing_metadata_realm_key(app_id, access_group);
928
        REQUIRE_FALSE(key_1);
×
929
        auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group);
930
        REQUIRE(key_2);
×
931
        auto key_3 = keychain::get_existing_metadata_realm_key(app_id, access_group);
932
        REQUIRE(key_3);
×
933
        REQUIRE(key_2 == key_3);
×
934
    }
935

936
    SECTION("keys are scoped to app ids") {
937
        auto key_1 = keychain::create_new_metadata_realm_key("app id 1", access_group);
938
        REQUIRE(key_1);
×
939
        auto key_2 = keychain::create_new_metadata_realm_key("app id 2", access_group);
940
        REQUIRE(key_2);
×
941
        REQUIRE(key_1 != key_2);
×
942
    }
943

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

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

973
        set_key(key, account, service);
974
        auto key_2 = keychain::get_existing_metadata_realm_key(app_id, {});
975
        REQUIRE(key_2 == key);
×
976

977
        // Key should have been copied to the preferred location
978
        REQUIRE(get_key(CFSTR("app id"), bundle_service.get(), key) == errSecSuccess);
×
979
        REQUIRE(key_2 == key);
×
980

981
        // Key should not have been deleted from the original location
982
        REQUIRE(get_key(account, service, key) == errSecSuccess);
×
983
        REQUIRE(key_2 == key);
×
984
    }
985
}
986
#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

© 2026 Coveralls, Inc