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

realm / realm-core / thomas.goyne_851

27 Feb 2025 12:51AM UTC coverage: 91.132% (+0.01%) from 91.119%
thomas.goyne_851

Pull #8072

Evergreen

tgoyne
Add some logging to a flakey audit test
Pull Request #8072: Enable automatic client reset handling for audit Realms

102776 of 181548 branches covered (56.61%)

77 of 79 new or added lines in 2 files covered. (97.47%)

81 existing lines in 14 files now uncovered.

217476 of 238639 relevant lines covered (91.13%)

5926136.9 hits per line

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

99.81
/test/object-store/audit.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2022 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 <util/event_loop.hpp>
20
#include <util/test_file.hpp>
21
#include <util/test_utils.hpp>
22
#include <util/sync/baas_admin_api.hpp>
23
#include <util/sync/flx_sync_harness.hpp>
24

25
#include <realm/dictionary.hpp>
26
#include <realm/list.hpp>
27
#include <realm/set.hpp>
28
#include <realm/sync/noinst/client_history_impl.hpp>
29
#include <realm/util/logger.hpp>
30

31
#include <realm/object-store/audit.hpp>
32
#include <realm/object-store/audit_serializer.hpp>
33
#include <realm/object-store/property.hpp>
34
#include <realm/object-store/object.hpp>
35
#include <realm/object-store/object_schema.hpp>
36
#include <realm/object-store/schema.hpp>
37
#include <realm/object-store/shared_realm.hpp>
38
#include <realm/object-store/impl/object_accessor_impl.hpp>
39
#include <realm/object-store/sync/sync_user.hpp>
40
#include <realm/object-store/sync/sync_manager.hpp>
41
#include <realm/object-store/sync/sync_session.hpp>
42
#include <realm/object-store/sync/mongo_client.hpp>
43
#include <realm/object-store/sync/mongo_database.hpp>
44
#include <realm/object-store/sync/mongo_collection.hpp>
45

46
#include <catch2/catch_all.hpp>
47
#include <external/json/json.hpp>
48

49
using namespace realm;
50
using namespace std::string_literals;
51
using Catch::Matchers::StartsWith;
52
using nlohmann::json;
53

54
static auto audit_logger =
55
#ifdef AUDIT_LOG_LEVEL
56
    std::make_shared<util::StderrLogger>(AUDIT_LOG_LEVEL);
57
#else
58
    std::make_shared<util::NullLogger>();
59
#endif
60

61
namespace {
62

63
struct AuditEvent {
64
    std::string activity;
65
    util::Optional<std::string> event;
66
    json data;
67
    util::Optional<std::string> raw_data;
68
    Timestamp timestamp;
69
    std::map<std::string, std::string> metadata;
70
};
71

72
std::ostream& operator<<(std::ostream& os, const std::vector<AuditEvent>& events)
73
{
1✔
74
    for (auto& event : events) {
6✔
75
        util::format(os, "%1: %2\n", event.event, event.data);
6✔
76
    }
6✔
77
    return os;
1✔
78
}
1✔
79

80
util::Optional<std::string> to_optional_string(StringData sd)
81
{
393✔
82
    return sd ? util::Optional<std::string>(sd) : none;
393✔
83
}
393✔
84

85
std::vector<AuditEvent> get_audit_events(TestSyncManager& manager, bool parse_events = true)
86
{
50✔
87
    // Wait for all sessions to be fully uploaded and then tear them down
88
    auto sync_manager = manager.sync_manager();
50✔
89
    REALM_ASSERT(sync_manager);
50✔
90
    auto sessions = sync_manager->get_all_sessions();
50✔
91
    for (auto& session : sessions) {
97✔
92
        // The realm user session has been manually closed, don't try to wait for it to sync
93
        // If the session is still active (in this case the audit session) wait for audit to complete
94
        if (session->state() == SyncSession::State::Active) {
97✔
95
            auto [promise, future] = util::make_promise_future<void>();
51✔
96
            session->wait_for_upload_completion([promise = std::move(promise)](Status) mutable {
51✔
97
                // Don't care if error occurred, just finish operation
98
                promise.emplace_value();
51✔
99
            });
51✔
100
            future.get();
51✔
101
        }
51✔
102
        session->shutdown_and_wait();
97✔
103
    }
97✔
104
    sync_manager->wait_for_sessions_to_terminate();
50✔
105

106
    // Stop the sync server so that we can safely inspect its Realm files
107
    auto& server = manager.sync_server();
50✔
108
    server.stop();
50✔
109

110
    std::vector<AuditEvent> events;
50✔
111

112
    // Iterate over all of the audit Realm files in the server's storage
113
    // directory, opening them in read-only mode (as they use Server history),
114
    // and slurp the audit events out of them.
115
    auto root = server.local_root_dir();
50✔
116
    std::string file_name;
50✔
117
    util::DirScanner dir(root);
50✔
118
    while (dir.next(file_name)) {
389✔
119
        StringData sd(file_name);
339✔
120
        if (!sd.begins_with("audit-") || !sd.ends_with(".realm"))
339✔
121
            continue;
275✔
122

123
        Group g(root + "/" + file_name);
64✔
124
        auto table = g.get_table("class_AuditEvent");
64✔
125
        if (!table)
64✔
126
            continue;
×
127

128
        ColKey activity_key, event_key, data_key, timestamp_key;
64✔
129
        std::vector<std::pair<std::string, ColKey>> metadata_keys;
64✔
130
        for (auto col_key : table->get_column_keys()) {
523✔
131
            auto name = table->get_column_name(col_key);
523✔
132
            if (name == "activity")
523✔
133
                activity_key = col_key;
64✔
134
            else if (name == "event")
459✔
135
                event_key = col_key;
64✔
136
            else if (name == "data")
395✔
137
                data_key = col_key;
64✔
138
            else if (name == "timestamp")
331✔
139
                timestamp_key = col_key;
64✔
140
            else if (name != "_id")
267✔
141
                metadata_keys.push_back({name, col_key});
203✔
142
        }
523✔
143

144
        for (auto& obj : *table) {
393✔
145
            AuditEvent event;
393✔
146
            event.activity = obj.get<StringData>(activity_key);
393✔
147
            event.event = to_optional_string(obj.get<StringData>(event_key));
393✔
148
            event.timestamp = obj.get<Timestamp>(timestamp_key);
393✔
149
            for (auto& [name, key] : metadata_keys) {
20,011✔
150
                if (auto sd = obj.get<StringData>(key))
20,011✔
151
                    event.metadata[name] = sd;
5,159✔
152
            }
20,011✔
153
            if (parse_events) {
393✔
154
                if (auto data = obj.get<StringData>(data_key))
289✔
155
                    event.data = json::parse(data.data(), data.data() + data.size());
289✔
156
            }
289✔
157
            else {
104✔
158
                if (auto data = obj.get<StringData>(data_key))
104✔
159
                    event.raw_data = std::string(data);
102✔
160
            }
104✔
161
            REALM_ASSERT(!event.timestamp.is_null() && event.timestamp != Timestamp(0, 0));
393✔
162
            events.push_back(std::move(event));
393✔
163
        }
393✔
164
    }
64✔
165

166
    return events;
50✔
167
}
50✔
168

169
void sort_events(std::vector<AuditEvent>& events)
170
{
10✔
171
    std::sort(events.begin(), events.end(), [](auto& a, auto& b) {
993✔
172
        return a.timestamp < b.timestamp;
993✔
173
    });
993✔
174
}
10✔
175

176
#if REALM_ENABLE_AUTH_TESTS
177
static std::vector<AuditEvent> get_audit_events_from_baas(TestAppSession& session, app::User& user,
178
                                                          size_t expected_count)
179
{
7✔
180
    static const std::set<std::string> nonmetadata_fields = {"activity", "event", "data", "realm_id"};
7✔
181

182
    auto documents = session.get_documents(user, "AuditEvent", expected_count);
7✔
183
    std::vector<AuditEvent> events;
7✔
184
    events.reserve(documents.size());
7✔
185
    for (auto doc : documents) {
59✔
186
        AuditEvent event;
59✔
187
        event.activity = static_cast<std::string>(doc["activity"]);
59✔
188
        event.timestamp = static_cast<Timestamp>(doc["timestamp"]);
59✔
189
        if (auto val = doc.find("event"); bool(val) && *val != bson::Bson()) {
59✔
190
            event.event = static_cast<std::string>(*val);
59✔
191
        }
59✔
192
        if (auto val = doc.find("data"); bool(val) && *val != bson::Bson()) {
59✔
193
            event.data = json::parse(static_cast<std::string>(*val));
59✔
194
        }
59✔
195
        for (auto [key, value] : doc) {
358✔
196
            if (value.type() == bson::Bson::Type::String && !nonmetadata_fields.count(key))
358✔
197
                event.metadata.insert({key, static_cast<std::string>(value)});
4✔
198
        }
358✔
199
        events.push_back(event);
59✔
200
    }
59✔
201
    sort_events(events);
7✔
202
    return events;
7✔
203
}
7✔
204
#endif
205

206
// Check that the given key is present and the value is null
207
#define REQUIRE_NULL(v, k)                                                                                           \
208
    do {                                                                                                             \
8✔
209
        REQUIRE(v.contains(k));                                                                                      \
8✔
210
        REQUIRE(v[k] == nullptr);                                                                                    \
8✔
211
    } while (0)
8✔
212

213
#define REQUIRE_SET_EQUAL(a, ...)                                                                                    \
214
    do {                                                                                                             \
19✔
215
        json actual = (a);                                                                                           \
19✔
216
        json expected = __VA_ARGS__;                                                                                 \
19✔
217
        std::sort(actual.begin(), actual.end());                                                                     \
19✔
218
        std::sort(expected.begin(), expected.end());                                                                 \
19✔
219
        REQUIRE(actual == expected);                                                                                 \
19✔
220
    } while (0)
19✔
221

222
class CustomSerializer : public AuditObjectSerializer {
223
public:
224
    const Obj* expected_obj = nullptr;
225
    bool error = false;
226
    size_t completion_count = 0;
227

228
    void to_json(json& out, const Obj& obj) override
229
    {
169✔
230
        if (error) {
169✔
231
            throw std::runtime_error("custom serialization error");
1✔
232
        }
1✔
233
        if (expected_obj) {
168✔
234
            REQUIRE(expected_obj->get_key() == obj.get_key());
2!
235
            REQUIRE(expected_obj->get_table()->get_key() == obj.get_table()->get_key());
2!
236
            out["obj"] = obj.get_key().value;
2✔
237
            out["table"] = obj.get_table()->get_key().value;
2✔
238
        }
2✔
239
        else {
166✔
240
            AuditObjectSerializer::to_json(out, obj);
166✔
241
        }
166✔
242
    }
168✔
243

244
    void scope_complete() override
245
    {
40✔
246
        ++completion_count;
40✔
247
    }
40✔
248
};
249

250
void assert_no_error(std::exception_ptr e)
251
{
484✔
252
    REALM_ASSERT(!e);
484✔
253
}
484✔
254

255
struct TestClock {
256
    std::atomic<int32_t> timestamp = 1000;
257
    TestClock()
258
    {
29✔
259
        audit_test_hooks::set_clock([&] {
331✔
260
            auto now = timestamp.fetch_add(1);
331✔
261
            return Timestamp(now, now);
331✔
262
        });
331✔
263
    }
29✔
264
    ~TestClock()
265
    {
29✔
266
        audit_test_hooks::set_clock(nullptr);
29✔
267
    }
29✔
268
};
269

270
} // namespace
271

272
TEST_CASE("audit object serialization", "[sync][pbs][audit]") {
36✔
273
    TestSyncManager test_session;
36✔
274
    SyncTestFile config(test_session, "parent");
36✔
275
    config.automatic_change_notifications = false;
36✔
276
    config.schema_version = 1;
36✔
277
    config.schema = Schema{
36✔
278
        {"object",
36✔
279
         {{"_id", PropertyType::Int, Property::IsPrimary{true}},
36✔
280

281
          {"int", PropertyType::Int | PropertyType::Nullable},
36✔
282
          {"bool", PropertyType::Bool | PropertyType::Nullable},
36✔
283
          {"string", PropertyType::String | PropertyType::Nullable},
36✔
284
          {"data", PropertyType::Data | PropertyType::Nullable},
36✔
285
          {"date", PropertyType::Date | PropertyType::Nullable},
36✔
286
          {"float", PropertyType::Float | PropertyType::Nullable},
36✔
287
          {"double", PropertyType::Double | PropertyType::Nullable},
36✔
288
          {"mixed", PropertyType::Mixed | PropertyType::Nullable},
36✔
289
          {"objectid", PropertyType::ObjectId | PropertyType::Nullable},
36✔
290
          {"decimal", PropertyType::Decimal | PropertyType::Nullable},
36✔
291
          {"uuid", PropertyType::UUID | PropertyType::Nullable},
36✔
292

293
          {"int list", PropertyType::Int | PropertyType::Nullable | PropertyType::Array},
36✔
294
          {"int set", PropertyType::Int | PropertyType::Nullable | PropertyType::Set},
36✔
295
          {"int dictionary", PropertyType::Int | PropertyType::Nullable | PropertyType::Dictionary},
36✔
296

297
          {"object", PropertyType::Object | PropertyType::Nullable, "target"},
36✔
298
          {"object list", PropertyType::Object | PropertyType::Array, "target"},
36✔
299
          {"object set", PropertyType::Object | PropertyType::Set, "target"},
36✔
300
          {"object dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary, "target"},
36✔
301

302
          {"embedded object", PropertyType::Object | PropertyType::Nullable, "embedded target"},
36✔
303
          {"embedded object list", PropertyType::Object | PropertyType::Array, "embedded target"},
36✔
304
          {"embedded object dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary,
36✔
305
           "embedded target"}}},
36✔
306
        {"target", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
36✔
307
        {"embedded target", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::Int}}}};
36✔
308
    config.audit_config = std::make_shared<AuditConfig>();
36✔
309
    config.audit_config->base_file_path = test_session.base_file_path();
36✔
310
    auto serializer = std::make_shared<CustomSerializer>();
36✔
311
    config.audit_config->serializer = serializer;
36✔
312
    config.audit_config->logger = audit_logger;
36✔
313
    auto realm = Realm::get_shared_realm(config);
36✔
314
    auto audit = realm->audit_context();
36✔
315
    REQUIRE(audit);
36!
316
    auto wait_for_completion = util::make_scope_exit([=]() noexcept {
36✔
317
        audit->wait_for_completion();
36✔
318
    });
36✔
319

320
    // We open in proper sync mode to let the audit context initialize from that,
321
    // but we don't actually want the realm to be synchronizing
322
    realm->sync_session()->close();
36✔
323

324
    auto table = realm->read_group().get_table("class_object");
36✔
325
    auto target_table = realm->read_group().get_table("class_target");
36✔
326
    CppContext context(realm);
36✔
327

328
    auto populate_object = [&](Obj& obj) {
36✔
329
        obj.set("int", 1);
4✔
330
        obj.set("bool", true);
4✔
331
        obj.set("string", "abc");
4✔
332
        obj.set("data", BinaryData("abc", 3));
4✔
333
        obj.set("date", Timestamp(123, 456));
4✔
334
        obj.set("float", 1.1f);
4✔
335
        obj.set("double", 2.2);
4✔
336
        obj.set("mixed", Mixed(10));
4✔
337
        obj.set("objectid", ObjectId("000000000000000000000001"));
4✔
338
        obj.set("uuid", UUID("00000000-0000-0000-0000-000000000001"));
4✔
339

340
        auto int_list = obj.get_list<util::Optional<int64_t>>("int list");
4✔
341
        int_list.add(1);
4✔
342
        int_list.add(2);
4✔
343
        int_list.add(3);
4✔
344
        int_list.add(none);
4✔
345

346
        auto int_set = obj.get_set<util::Optional<int64_t>>("int set");
4✔
347
        int_set.insert(1);
4✔
348
        int_set.insert(2);
4✔
349
        int_set.insert(3);
4✔
350
        int_set.insert(none);
4✔
351

352
        auto int_dictionary = obj.get_dictionary("int dictionary");
4✔
353
        int_dictionary.insert("1", 1);
4✔
354
        int_dictionary.insert("2", 2);
4✔
355
        int_dictionary.insert("3", 3);
4✔
356
        int_dictionary.insert("4", none);
4✔
357

358
        auto obj_list = obj.get_linklist("object list");
4✔
359
        obj_list.add(target_table->create_object_with_primary_key(1).set_all(1).get_key());
4✔
360
        obj_list.add(target_table->create_object_with_primary_key(2).set_all(2).get_key());
4✔
361
        obj_list.add(target_table->create_object_with_primary_key(3).set_all(3).get_key());
4✔
362

363
        auto obj_set = obj.get_linkset(obj.get_table()->get_column_key("object set"));
4✔
364
        obj_set.insert(target_table->create_object_with_primary_key(4).set_all(4).get_key());
4✔
365
        obj_set.insert(target_table->create_object_with_primary_key(5).set_all(5).get_key());
4✔
366
        obj_set.insert(target_table->create_object_with_primary_key(6).set_all(6).get_key());
4✔
367

368
        auto obj_dict = obj.get_dictionary("object dictionary");
4✔
369
        obj_dict.insert("a", target_table->create_object_with_primary_key(7).set_all(7).get_key());
4✔
370
        obj_dict.insert("b", target_table->create_object_with_primary_key(8).set_all(8).get_key());
4✔
371
        obj_dict.insert("c", target_table->create_object_with_primary_key(9).set_all(9).get_key());
4✔
372

373
        auto embedded_list = obj.get_linklist("embedded object list");
4✔
374
        embedded_list.create_and_insert_linked_object(0).set_all(1);
4✔
375
        embedded_list.create_and_insert_linked_object(1).set_all(2);
4✔
376
        embedded_list.create_and_insert_linked_object(2).set_all(3);
4✔
377

378
        auto embedded_dict = obj.get_dictionary("embedded object dictionary");
4✔
379
        embedded_dict.create_and_insert_linked_object("d").set_all(4);
4✔
380
        embedded_dict.create_and_insert_linked_object("e").set_all(5);
4✔
381
        embedded_dict.create_and_insert_linked_object("f").set_all(6);
4✔
382

383
        return obj;
4✔
384
    };
4✔
385

386
    auto validate_default_values = [](auto& value) {
36✔
387
        REQUIRE(value.size() == 21);
4!
388
        REQUIRE(value["_id"] == 2);
4!
389
        REQUIRE(value["int"] == 1);
4!
390
        REQUIRE(value["bool"] == true);
4!
391
        REQUIRE(value["string"] == "abc");
4!
392
        REQUIRE_FALSE(value.contains("data"));
4!
393
        REQUIRE(value["date"] == "1970-01-01T00:02:03.000Z");
4!
394
        REQUIRE(value["float"] == 1.1f);
4!
395
        REQUIRE(value["double"] == 2.2);
4!
396
        REQUIRE(value["mixed"] == 10);
4!
397
        REQUIRE(value["objectid"] == "000000000000000000000001");
4!
398
        REQUIRE(value["uuid"] == "00000000-0000-0000-0000-000000000001");
4!
399
        REQUIRE_NULL(value, "object");
4!
400
        REQUIRE_NULL(value, "embedded object");
4!
401
        REQUIRE(value["int list"] == json({1, 2, 3, nullptr}));
4!
402
        REQUIRE_SET_EQUAL(value["int set"], {1, 2, 3, nullptr});
4!
403
        REQUIRE(value["int dictionary"] == json({{"1", 1}, {"2", 2}, {"3", 3}, {"4", nullptr}}));
4!
404
        REQUIRE(value["object list"] == json({1, 2, 3}));
4!
405
        REQUIRE_SET_EQUAL(value["object set"], {4, 5, 6});
4!
406
    };
4✔
407

408
    SECTION("default object serialization") {
36✔
409
        realm->begin_transaction();
1✔
410
        auto obj = table->create_object_with_primary_key(2);
1✔
411
        populate_object(obj);
1✔
412
        realm->commit_transaction();
1✔
413

414
        auto scope = audit->begin_scope("scope");
1✔
415
        Object object(realm, obj);
1✔
416
        audit->end_scope(scope, assert_no_error);
1✔
417
        audit->wait_for_completion();
1✔
418

419
        auto events = get_audit_events(test_session);
1✔
420
        REQUIRE(events.size() == 1);
1!
421
        auto& event = events[0];
1✔
422
        REQUIRE(event.event == "read");
1!
423
        REQUIRE(event.activity == "scope");
1!
424
        REQUIRE(!event.timestamp.is_null());
1!
425

426
        REQUIRE(event.data["type"] == "object");
1!
427
        auto value = event.data["value"];
1✔
428
        REQUIRE(value.size() == 1);
1!
429
        validate_default_values(value[0]);
1✔
430
    }
1✔
431

432
    SECTION("custom object serialization") {
36✔
433
        realm->begin_transaction();
1✔
434
        auto obj1 = table->create_object_with_primary_key(2);
1✔
435
        auto obj2 = table->create_object_with_primary_key(3);
1✔
436
        realm->commit_transaction();
1✔
437

438
        serializer->expected_obj = &obj1;
1✔
439

440
        auto scope = audit->begin_scope("scope 1");
1✔
441
        Object(realm, obj1);
1✔
442
        audit->end_scope(scope, assert_no_error);
1✔
443
        audit->wait_for_completion();
1✔
444
        REQUIRE(serializer->completion_count == 1);
1!
445

446
        scope = audit->begin_scope("empty scope");
1✔
447
        audit->end_scope(scope, assert_no_error);
1✔
448
        audit->wait_for_completion();
1✔
449
        REQUIRE(serializer->completion_count == 2);
1!
450

451
        serializer->expected_obj = &obj2;
1✔
452

453
        scope = audit->begin_scope("scope 2");
1✔
454
        Object(realm, obj2);
1✔
455
        audit->end_scope(scope, assert_no_error);
1✔
456
        audit->wait_for_completion();
1✔
457
        REQUIRE(serializer->completion_count == 3);
1!
458

459
        auto events = get_audit_events(test_session);
1✔
460
        REQUIRE(events.size() == 2);
1!
461

462
        REQUIRE(events[0].activity == "scope 1");
1!
463
        REQUIRE(events[1].activity == "scope 2");
1!
464
        REQUIRE(events[0].data ==
1!
465
                json({{"type", "object"},
1✔
466
                      {"value", json::array({{{"obj", obj1.get_key().value}, {"table", table->get_key().value}}})}}));
1✔
467
        REQUIRE(events[1].data ==
1!
468
                json({{"type", "object"},
1✔
469
                      {"value", json::array({{{"obj", obj2.get_key().value}, {"table", table->get_key().value}}})}}));
1✔
470
    }
1✔
471

472
    SECTION("custom serialization error reporting") {
36✔
473
        serializer->error = true;
1✔
474

475
        realm->begin_transaction();
1✔
476
        auto obj = table->create_object_with_primary_key(2);
1✔
477
        realm->commit_transaction();
1✔
478
        auto scope = audit->begin_scope("scope");
1✔
479
        Object(realm, obj);
1✔
480
        audit->end_scope(scope, [](auto error) {
1✔
481
            REQUIRE(error);
1!
482
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "custom serialization error");
1✔
483
        });
1✔
484
        audit->wait_for_completion();
1✔
485
    }
1✔
486

487
    SECTION("write transaction serialization") {
36✔
488
        SECTION("create object") {
6✔
489
            auto scope = audit->begin_scope("scope");
1✔
490
            realm->begin_transaction();
1✔
491
            auto obj = table->create_object_with_primary_key(2);
1✔
492
            populate_object(obj);
1✔
493
            realm->commit_transaction();
1✔
494
            audit->end_scope(scope, assert_no_error);
1✔
495
            audit->wait_for_completion();
1✔
496

497
            auto events = get_audit_events(test_session);
1✔
498
            REQUIRE(events.size() == 1);
1!
499
            auto& event = events[0];
1✔
500
            REQUIRE(event.event == "write");
1!
501
            REQUIRE(event.activity == "scope");
1!
502
            REQUIRE(!event.timestamp.is_null());
1!
503

504
            REQUIRE(event.data.size() == 2);
1!
505
            auto& object_changes = event.data["object"];
1✔
506
            REQUIRE(object_changes.size() == 1);
1!
507
            REQUIRE(object_changes["insertions"].size() == 1);
1!
508
            validate_default_values(object_changes["insertions"][0]);
1✔
509

510
            // target table should have 9 insertions with _id == value
511
            REQUIRE(event.data["target"]["insertions"].size() == 9);
1!
512
            for (int i = 0; i < 9; ++i) {
10✔
513
                REQUIRE(event.data["target"]["insertions"][i] == json({{"_id", i + 1}, {"value", i + 1}}));
9!
514
            }
9✔
515
        }
1✔
516

517
        SECTION("modify object") {
6✔
518
            realm->begin_transaction();
1✔
519
            auto obj = table->create_object_with_primary_key(2);
1✔
520
            populate_object(obj);
1✔
521
            realm->commit_transaction();
1✔
522

523
            auto scope = audit->begin_scope("scope");
1✔
524
            realm->begin_transaction();
1✔
525
            obj.set("int", 3);
1✔
526
            obj.set("bool", true);
1✔
527
            realm->commit_transaction();
1✔
528
            audit->end_scope(scope, assert_no_error);
1✔
529
            audit->wait_for_completion();
1✔
530

531
            auto events = get_audit_events(test_session);
1✔
532
            REQUIRE(events.size() == 1);
1!
533
            auto& event = events[0];
1✔
534
            REQUIRE(event.data.size() == 1);
1!
535
            REQUIRE(event.data["object"].size() == 1);
1!
536
            REQUIRE(event.data["object"]["modifications"].size() == 1);
1!
537
            auto& modifications = event.data["object"]["modifications"][0];
1✔
538
            REQUIRE(modifications.size() == 2);
1!
539
            REQUIRE(modifications["newValue"].size() == 1);
1!
540
            REQUIRE(modifications["newValue"]["int"] == 3);
1!
541
            // note: bool is not reported because it was assigned to itself
542
            validate_default_values(modifications["oldValue"]);
1✔
543
        }
1✔
544

545
        SECTION("delete object") {
6✔
546
            realm->begin_transaction();
1✔
547
            auto obj = table->create_object_with_primary_key(2);
1✔
548
            populate_object(obj);
1✔
549
            realm->commit_transaction();
1✔
550

551
            auto scope = audit->begin_scope("scope");
1✔
552
            realm->begin_transaction();
1✔
553
            obj.remove();
1✔
554
            realm->commit_transaction();
1✔
555
            audit->end_scope(scope, assert_no_error);
1✔
556
            audit->wait_for_completion();
1✔
557

558
            auto events = get_audit_events(test_session);
1✔
559
            REQUIRE(events.size() == 1);
1!
560
            auto& event = events[0];
1✔
561
            REQUIRE(event.data.size() == 1);
1!
562
            REQUIRE(event.data["object"].size() == 1);
1!
563
            REQUIRE(event.data["object"]["deletions"].size() == 1);
1!
564
            validate_default_values(event.data["object"]["deletions"][0]);
1✔
565
        }
1✔
566

567
        SECTION("delete embedded object") {
6✔
568
            realm->begin_transaction();
1✔
569
            auto obj = table->create_object_with_primary_key(2);
1✔
570
            obj.create_and_set_linked_object(obj.get_table()->get_column_key("embedded object")).set_all(100);
1✔
571
            realm->commit_transaction();
1✔
572

573
            auto scope = audit->begin_scope("scope");
1✔
574
            realm->begin_transaction();
1✔
575
            obj.get_linked_object("embedded object").remove();
1✔
576
            realm->commit_transaction();
1✔
577
            audit->end_scope(scope, assert_no_error);
1✔
578
            audit->wait_for_completion();
1✔
579

580
            auto events = get_audit_events(test_session);
1✔
581
            REQUIRE(events.size() == 1);
1!
582
            REQUIRE(events[0].data.size() == 1);
1!
583
            REQUIRE(events[0].data["object"].size() == 1);
1!
584
            REQUIRE(events[0].data["object"]["modifications"].size() == 1);
1!
585
            auto& modification = events[0].data["object"]["modifications"][0];
1✔
586
            REQUIRE(modification["newValue"] == json({{"embedded object", nullptr}}));
1!
587
            REQUIRE(modification["oldValue"]["embedded object"] == json({{"value", 100}}));
1!
588
        }
1✔
589

590
        SECTION("mixed changes") {
6✔
591
            realm->begin_transaction();
1✔
592
            std::vector<Obj> objects;
1✔
593
            for (int i = 0; i < 5; ++i)
6✔
594
                objects.push_back(target_table->create_object_with_primary_key(i).set_all(i));
5✔
595
            realm->commit_transaction();
1✔
596

597
            auto scope = audit->begin_scope("scope");
1✔
598
            realm->begin_transaction();
1✔
599

600
            // Mutate then delete should not report the mutate
601
            objects[0].set("value", 100);
1✔
602
            objects[1].set("value", 100);
1✔
603
            objects[2].set("value", 100);
1✔
604
            objects[1].remove();
1✔
605

606
            // Insert then mutate should not report the mutate
607
            auto obj = target_table->create_object_with_primary_key(20);
1✔
608
            obj.set("value", 100);
1✔
609

610
            // Insert then delete should not report the insert or delete
611
            auto obj2 = target_table->create_object_with_primary_key(21);
1✔
612
            obj2.remove();
1✔
613

614
            realm->commit_transaction();
1✔
615
            audit->end_scope(scope, assert_no_error);
1✔
616
            audit->wait_for_completion();
1✔
617

618
            auto events = get_audit_events(test_session);
1✔
619
            REQUIRE(events.size() == 1);
1!
620
            auto& event = events[0];
1✔
621
            REQUIRE(event.data.size() == 1);
1!
622
            auto& data = event.data["target"];
1✔
623
            REQUIRE(data.size() == 3);
1!
624
            REQUIRE(data["deletions"] == json({{{"_id", 1}, {"value", 1}}}));
1!
625
            REQUIRE(data["insertions"] == json({{{"_id", 20}, {"value", 100}}}));
1!
626
            REQUIRE(data["modifications"] ==
1!
627
                    json({{{"oldValue", {{"_id", 0}, {"value", 0}}}, {"newValue", {{"value", 100}}}},
1✔
628
                          {{"oldValue", {{"_id", 2}, {"value", 2}}}, {"newValue", {{"value", 100}}}}}));
1✔
629
        }
1✔
630

631
        SECTION("empty write transactions do not produce an event") {
6✔
632
            auto scope = audit->begin_scope("scope");
1✔
633
            realm->begin_transaction();
1✔
634
            realm->commit_transaction();
1✔
635
            audit->end_scope(scope, assert_no_error);
1✔
636
            audit->wait_for_completion();
1✔
637

638
            REQUIRE(get_audit_events(test_session).empty());
1!
639
        }
1✔
640
    }
6✔
641

642
    SECTION("empty query") {
36✔
643
        auto scope = audit->begin_scope("scope");
1✔
644
        Results(realm, table->where()).snapshot();
1✔
645
        audit->end_scope(scope, assert_no_error);
1✔
646
        audit->wait_for_completion();
1✔
647
        REQUIRE(get_audit_events(test_session).empty());
1!
648
    }
1✔
649

650
    SECTION("non-empty query") {
36✔
651
        realm->begin_transaction();
5✔
652
        for (int64_t i = 0; i < 10; ++i) {
55✔
653
            table->create_object_with_primary_key(i);
50✔
654
            target_table->create_object_with_primary_key(i);
50✔
655
        }
50✔
656
        realm->commit_transaction();
5✔
657

658
        SECTION("query counts as a read on all objects matching the query") {
5✔
659
            auto scope = audit->begin_scope("scope");
1✔
660
            Results(realm, table->where().less(table->get_column_key("_id"), 5)).snapshot();
1✔
661
            audit->end_scope(scope, assert_no_error);
1✔
662
            audit->wait_for_completion();
1✔
663
            auto events = get_audit_events(test_session);
1✔
664
            REQUIRE(events.size() == 1);
1!
665
            REQUIRE(events[0].data["value"].size() == 5);
1!
666
        }
1✔
667

668
        SECTION("subsequent reads on the same table are folded into the query") {
5✔
669
            auto scope = audit->begin_scope("scope");
1✔
670
            Results(realm, table->where().less(table->get_column_key("_id"), 5)).snapshot();
1✔
671
            Object(realm, table->get_object(3)); // does not produce any new audit data
1✔
672
            Object(realm, table->get_object(7)); // adds this object to the query's event
1✔
673
            audit->end_scope(scope, assert_no_error);
1✔
674
            audit->wait_for_completion();
1✔
675
            auto events = get_audit_events(test_session);
1✔
676
            REQUIRE(events.size() == 1);
1!
677
            REQUIRE(events[0].data["value"].size() == 6);
1!
678
        }
1✔
679

680
        SECTION("reads on different tables are not folded into query") {
5✔
681
            auto scope = audit->begin_scope("scope");
1✔
682
            Results(realm, table->where().less(table->get_column_key("_id"), 5)).snapshot();
1✔
683
            Object(realm, target_table->get_object(3));
1✔
684
            audit->end_scope(scope, assert_no_error);
1✔
685
            audit->wait_for_completion();
1✔
686
            auto events = get_audit_events(test_session);
1✔
687
            REQUIRE(events.size() == 2);
1!
688
            REQUIRE(events[0].data["value"].size() == 5);
1!
689
            REQUIRE(events[1].data["value"].size() == 1);
1!
690
        }
1✔
691

692
        SECTION("reads on same table following a read on a different table are not folded into query") {
5✔
693
            auto scope = audit->begin_scope("scope");
1✔
694
            Results(realm, table->where().less(table->get_column_key("_id"), 5)).snapshot();
1✔
695
            Object(realm, target_table->get_object(3));
1✔
696
            Object(realm, table->get_object(3));
1✔
697
            audit->end_scope(scope, assert_no_error);
1✔
698
            audit->wait_for_completion();
1✔
699
            auto events = get_audit_events(test_session);
1✔
700
            REQUIRE(events.size() == 3);
1!
701
            REQUIRE(events[0].data["value"].size() == 5);
1!
702
            REQUIRE(events[1].data["value"].size() == 1);
1!
703
            REQUIRE(events[2].data["value"].size() == 1);
1!
704
        }
1✔
705

706
        SECTION("reads with intervening writes are not combined") {
5✔
707
            auto scope = audit->begin_scope("scope");
1✔
708
            Results(realm, table->where().less(table->get_column_key("_id"), 5)).snapshot();
1✔
709
            realm->begin_transaction();
1✔
710
            realm->commit_transaction();
1✔
711
            Object(realm, table->get_object(3));
1✔
712
            audit->end_scope(scope, assert_no_error);
1✔
713
            audit->wait_for_completion();
1✔
714
            auto events = get_audit_events(test_session);
1✔
715
            REQUIRE(events.size() == 2);
1!
716
            REQUIRE(events[0].data["value"].size() == 5);
1!
717
            REQUIRE(events[1].data["value"].size() == 1);
1!
718
        }
1✔
719
    }
5✔
720

721
    SECTION("query on list of objects") {
36✔
722
        realm->begin_transaction();
1✔
723
        auto obj = table->create_object_with_primary_key(2);
1✔
724
        auto list = obj.get_linklist("object list");
1✔
725
        for (int64_t i = 0; i < 10; ++i)
11✔
726
            list.add(target_table->create_object_with_primary_key(i).set_all(i * 2).get_key());
10✔
727
        realm->commit_transaction();
1✔
728

729
        auto scope = audit->begin_scope("scope");
1✔
730
        Object object(realm, obj);
1✔
731
        auto obj_list = util::any_cast<List>(object.get_property_value<std::any>(context, "object list"));
1✔
732
        obj_list.filter(target_table->where().greater(target_table->get_column_key("value"), 10)).snapshot();
1✔
733
        audit->end_scope(scope, assert_no_error);
1✔
734
        audit->wait_for_completion();
1✔
735

736
        auto events = get_audit_events(test_session);
1✔
737
        REQUIRE(events.size() == 2);
1!
738
        REQUIRE(events[0].data["type"] == "object");
1!
739
        REQUIRE(events[0].data["value"][0]["object list"] == json({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
1!
740
        REQUIRE(events[1].data["type"] == "target");
1!
741
        REQUIRE(events[1].data["value"] == json({
1!
742
                                               {{"_id", 6}, {"value", 12}},
1✔
743
                                               {{"_id", 7}, {"value", 14}},
1✔
744
                                               {{"_id", 8}, {"value", 16}},
1✔
745
                                               {{"_id", 9}, {"value", 18}},
1✔
746
                                           }));
1✔
747
    }
1✔
748

749
    SECTION("link access tracking") {
36✔
750
        realm->begin_transaction();
16✔
751
        table->create_object_with_primary_key(1);
16✔
752
        target_table->create_object_with_primary_key(0);
16✔
753
        auto obj = table->create_object_with_primary_key(2);
16✔
754
        obj.set("object", target_table->create_object_with_primary_key(1).set_all(1).get_key());
16✔
755
        obj.create_and_set_linked_object(table->get_column_key("embedded object")).set_all(200);
16✔
756

757
        auto obj_list = obj.get_linklist("object list");
16✔
758
        obj_list.add(target_table->create_object_with_primary_key(3).set_all(10).get_key());
16✔
759
        obj_list.add(target_table->create_object_with_primary_key(4).set_all(20).get_key());
16✔
760
        obj_list.add(target_table->create_object_with_primary_key(5).set_all(30).get_key());
16✔
761

762
        auto obj_set = obj.get_linkset(obj.get_table()->get_column_key("object set"));
16✔
763
        obj_set.insert(target_table->create_object_with_primary_key(6).set_all(40).get_key());
16✔
764
        obj_set.insert(target_table->create_object_with_primary_key(7).set_all(50).get_key());
16✔
765
        obj_set.insert(target_table->create_object_with_primary_key(8).set_all(60).get_key());
16✔
766

767
        auto obj_dict = obj.get_dictionary("object dictionary");
16✔
768
        obj_dict.insert("a", target_table->create_object_with_primary_key(9).set_all(90).get_key());
16✔
769
        obj_dict.insert("b", target_table->create_object_with_primary_key(10).set_all(100).get_key());
16✔
770
        obj_dict.insert("c", target_table->create_object_with_primary_key(11).set_all(110).get_key());
16✔
771
        realm->commit_transaction();
16✔
772

773
        SECTION("objects are serialized as just primary key by default") {
16✔
774
            auto scope = audit->begin_scope("scope");
1✔
775
            Object object(realm, obj);
1✔
776
            audit->end_scope(scope, assert_no_error);
1✔
777
            audit->wait_for_completion();
1✔
778

779
            auto events = get_audit_events(test_session);
1✔
780
            REQUIRE(events.size() == 1);
1!
781
            auto& value = events[0].data["value"][0];
1✔
782
            REQUIRE(value["object"] == 1);
1!
783
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
784
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
785
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
786
        }
1✔
787

788
        SECTION("embedded objects are always full object") {
16✔
789
            auto scope = audit->begin_scope("scope");
1✔
790
            Object object(realm, obj);
1✔
791
            audit->end_scope(scope, assert_no_error);
1✔
792
            audit->wait_for_completion();
1✔
793

794
            auto events = get_audit_events(test_session);
1✔
795
            REQUIRE(events.size() == 1);
1!
796
            REQUIRE(events[0].data["value"][0]["embedded object"] == json({{"value", 200}}));
1!
797
        }
1✔
798

799
        SECTION("links followed serialize the full object") {
16✔
800
            auto scope = audit->begin_scope("scope");
1✔
801
            Object object(realm, obj);
1✔
802
            object.get_property_value<std::any>(context, "object");
1✔
803
            audit->end_scope(scope, assert_no_error);
1✔
804
            audit->wait_for_completion();
1✔
805

806
            auto events = get_audit_events(test_session);
1✔
807
            REQUIRE(events.size() == 2);
1!
808
            auto& value = events[0].data["value"][0];
1✔
809
            REQUIRE(value["object"] == json({{"_id", 1}, {"value", 1}}));
1!
810
            REQUIRE(events[1].data["value"][0] == json({{"_id", 1}, {"value", 1}}));
1!
811

812
            // Other fields are left in pk form
813
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
814
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
815
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
816
        }
1✔
817

818
        SECTION("instantiating a collection accessor does not count as a read") {
16✔
819
            auto scope = audit->begin_scope("scope");
1✔
820
            Object object(realm, obj);
1✔
821
            util::any_cast<List>(object.get_property_value<std::any>(context, "object list"));
1✔
822
            util::any_cast<object_store::Set>(object.get_property_value<std::any>(context, "object set"));
1✔
823
            util::any_cast<object_store::Dictionary>(
1✔
824
                object.get_property_value<std::any>(context, "object dictionary"));
1✔
825
            audit->end_scope(scope, assert_no_error);
1✔
826
            audit->wait_for_completion();
1✔
827

828
            auto events = get_audit_events(test_session);
1✔
829
            REQUIRE(events.size() == 1);
1!
830
            auto& value = events[0].data["value"][0];
1✔
831
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
832
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
833
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
834
        }
1✔
835

836
        SECTION("accessing any value from a collection serializes full objects for the entire collection") {
16✔
837
            SECTION("list") {
8✔
838
                auto scope = audit->begin_scope("scope");
2✔
839
                Object object(realm, obj);
2✔
840
                auto list = util::any_cast<List>(object.get_property_value<std::any>(context, "object list"));
2✔
841
                SECTION("get()") {
2✔
842
                    list.get(1);
1✔
843
                }
1✔
844
                SECTION("get_any()") {
2✔
845
                    list.get_any(1);
1✔
846
                }
1✔
847
                audit->end_scope(scope, assert_no_error);
2✔
848
                audit->wait_for_completion();
2✔
849

850
                auto events = get_audit_events(test_session);
2✔
851
                REQUIRE(events.size() == 2);
2!
852
                auto& value = events[0].data["value"][0];
2✔
853
                REQUIRE(
2!
854
                    value["object list"] ==
2✔
855
                    json({{{"_id", 3}, {"value", 10}}, {{"_id", 4}, {"value", 20}}, {{"_id", 5}, {"value", 30}}}));
2✔
856
                REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
2!
857
                REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
2!
858
            }
2✔
859

860
            SECTION("set") {
8✔
861
                auto scope = audit->begin_scope("scope");
2✔
862
                Object object(realm, obj);
2✔
863
                auto set =
2✔
864
                    util::any_cast<object_store::Set>(object.get_property_value<std::any>(context, "object set"));
2✔
865
                SECTION("get()") {
2✔
866
                    set.get(1);
1✔
867
                }
1✔
868
                SECTION("get_any()") {
2✔
869
                    set.get_any(1);
1✔
870
                }
1✔
871
                audit->end_scope(scope, assert_no_error);
2✔
872
                audit->wait_for_completion();
2✔
873

874
                auto events = get_audit_events(test_session);
2✔
875
                REQUIRE(events.size() == 2);
2!
876
                auto& value = events[0].data["value"][0];
2✔
877
                REQUIRE_SET_EQUAL(
2!
878
                    value["object set"],
2✔
879
                    json({{{"_id", 6}, {"value", 40}}, {{"_id", 7}, {"value", 50}}, {{"_id", 8}, {"value", 60}}}));
2✔
880
                REQUIRE(value["object list"] == json({3, 4, 5}));
2!
881
                REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
2!
882
            }
2✔
883

884
            SECTION("dictionary") {
8✔
885
                auto scope = audit->begin_scope("scope");
4✔
886
                Object object(realm, obj);
4✔
887
                auto dict = util::any_cast<object_store::Dictionary>(
4✔
888
                    object.get_property_value<std::any>(context, "object dictionary"));
4✔
889
                SECTION("get_object()") {
4✔
890
                    dict.get_object("b");
1✔
891
                }
1✔
892
                SECTION("get_any(string)") {
4✔
893
                    dict.get_any("b");
1✔
894
                }
1✔
895
                SECTION("get_any(index)") {
4✔
896
                    const_cast<const object_store::Dictionary&>(dict).get_any(size_t(1));
1✔
897
                }
1✔
898
                SECTION("try_get_any()") {
4✔
899
                    dict.try_get_any("b");
1✔
900
                }
1✔
901
                audit->end_scope(scope, assert_no_error);
4✔
902
                audit->wait_for_completion();
4✔
903

904
                auto events = get_audit_events(test_session);
4✔
905
                REQUIRE(events.size() == 2);
4!
906
                auto& value = events[0].data["value"][0];
4✔
907
                REQUIRE(value["object list"] == json({3, 4, 5}));
4!
908
                REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
4!
909
                REQUIRE(value["object dictionary"] == json({{"a", {{"_id", 9}, {"value", 90}}},
4!
910
                                                            {"b", {{"_id", 10}, {"value", 100}}},
4✔
911
                                                            {"c", {{"_id", 11}, {"value", 110}}}}));
4✔
912
            }
4✔
913
        }
8✔
914

915
        SECTION(
16✔
916
            "link access on an object read outside of a scope does not produce a read on the parent in the scope") {
1✔
917
            Object object(realm, obj);
1✔
918
            auto scope = audit->begin_scope("scope");
1✔
919
            object.get_property_value<std::any>(context, "object");
1✔
920
            audit->end_scope(scope, assert_no_error);
1✔
921
            audit->wait_for_completion();
1✔
922

923
            auto events = get_audit_events(test_session);
1✔
924
            REQUIRE(events.size() == 1);
1!
925
            auto& event = events[0];
1✔
926
            REQUIRE(event.event == "read");
1!
927
            REQUIRE(event.data["type"] == "target");
1!
928
        }
1✔
929

930
        SECTION("link access in a different scope from the object do not expand linked object in parent read") {
16✔
931
            auto scope = audit->begin_scope("scope 1");
1✔
932
            Object object(realm, obj);
1✔
933
            audit->end_scope(scope, assert_no_error);
1✔
934

935
            scope = audit->begin_scope("scope 2");
1✔
936
            object.get_property_value<std::any>(context, "object");
1✔
937
            audit->end_scope(scope, assert_no_error);
1✔
938
            audit->wait_for_completion();
1✔
939

940
            auto events = get_audit_events(test_session);
1✔
941
            REQUIRE(events.size() == 2);
1!
942
            REQUIRE(events[0].activity == "scope 1");
1!
943
            REQUIRE(events[0].data["type"] == "object");
1!
944
            REQUIRE(events[1].activity == "scope 2");
1!
945
            REQUIRE(events[1].data["type"] == "target");
1!
946
            REQUIRE(events[0].data["value"][0]["object"] == 1);
1!
947
        }
1✔
948

949
        SECTION("link access tracking is reset between scopes") {
16✔
950
            auto scope = audit->begin_scope("scope 1");
1✔
951
            Object object(realm, obj);
1✔
952
            object.get_property_value<std::any>(context, "object");
1✔
953
            audit->end_scope(scope, assert_no_error);
1✔
954

955
            scope = audit->begin_scope("scope 2");
1✔
956
            // Perform two unrelated events so that the read on `obj` is at
957
            // an event index after the link access in the previous scope
958
            Object(realm, target_table->get_object(obj_set.get(0)));
1✔
959
            Object(realm, target_table->get_object(obj_set.get(1)));
1✔
960
            Object(realm, obj);
1✔
961
            audit->end_scope(scope, assert_no_error);
1✔
962
            audit->wait_for_completion();
1✔
963

964
            auto events = get_audit_events(test_session);
1✔
965
            REQUIRE(events.size() == 5);
1!
966
            REQUIRE(events[0].activity == "scope 1");
1!
967
            REQUIRE(events[1].activity == "scope 1");
1!
968
            REQUIRE(events[2].activity == "scope 2");
1!
969
            REQUIRE(events[3].activity == "scope 2");
1!
970
            REQUIRE(events[4].activity == "scope 2");
1!
971

972
            REQUIRE(events[0].data["type"] == "object");
1!
973
            REQUIRE(events[1].data["type"] == "target");
1!
974
            REQUIRE(events[2].data["type"] == "target");
1!
975
            REQUIRE(events[3].data["type"] == "target");
1!
976
            REQUIRE(events[4].data["type"] == "object");
1!
977

978
            // First link should be expanded, second, should not
979
            REQUIRE(events[0].data["value"][0]["object"] == json({{"_id", 1}, {"value", 1}}));
1!
980
            REQUIRE(events[4].data["value"][0]["object"] == 1);
1!
981
        }
1✔
982

983
        SECTION("read on the parent after the link access do not expand the linked object") {
16✔
984
            Object object(realm, obj);
1✔
985

986
            auto scope = audit->begin_scope("scope");
1✔
987
            object.get_property_value<std::any>(context, "object");
1✔
988
            Object(realm, obj);
1✔
989
            audit->end_scope(scope, assert_no_error);
1✔
990
            audit->wait_for_completion();
1✔
991

992
            auto events = get_audit_events(test_session);
1✔
993
            REQUIRE(events.size() == 2);
1!
994
            REQUIRE(events[1].data["value"][0]["object"] == 1);
1!
995
        }
1✔
996
    }
16✔
997

998
    SECTION("read on newly created object") {
36✔
999
        realm->begin_transaction();
1✔
1000
        auto scope = audit->begin_scope("scope");
1✔
1001
        Object object(realm, table->create_object_with_primary_key(100));
1✔
1002
        Results(realm, table->where()).snapshot();
1✔
1003
        audit->end_scope(scope, assert_no_error);
1✔
1004
        realm->commit_transaction();
1✔
1005
        audit->wait_for_completion();
1✔
1006

1007
        auto events = get_audit_events(test_session);
1✔
1008
        REQUIRE(events.empty());
1!
1009
    }
1✔
1010

1011
    SECTION("query matching both new and existing objects") {
36✔
1012
        realm->begin_transaction();
1✔
1013
        table->create_object_with_primary_key(1);
1✔
1014
        realm->commit_transaction();
1✔
1015

1016
        realm->begin_transaction();
1✔
1017
        table->create_object_with_primary_key(2);
1✔
1018
        auto scope = audit->begin_scope("scope");
1✔
1019
        Results(realm, table->where()).snapshot();
1✔
1020
        audit->end_scope(scope, assert_no_error);
1✔
1021
        realm->commit_transaction();
1✔
1022
        audit->wait_for_completion();
1✔
1023

1024
        auto events = get_audit_events(test_session);
1✔
1025
        REQUIRE(events.size() == 1);
1!
1026
        REQUIRE(events[0].data["value"].size() == 1);
1!
1027
    }
1✔
1028

1029
    SECTION("reads mixed with deletions") {
36✔
1030
        realm->begin_transaction();
2✔
1031
        table->create_object_with_primary_key(1);
2✔
1032
        auto obj2 = table->create_object_with_primary_key(2);
2✔
1033
        auto obj3 = table->create_object_with_primary_key(3);
2✔
1034
        realm->commit_transaction();
2✔
1035

1036
        SECTION("reads of objects that are subsequently deleted are still reported") {
2✔
1037
            auto scope = audit->begin_scope("scope");
1✔
1038
            realm->begin_transaction();
1✔
1039
            Object(realm, obj2);
1✔
1040
            obj2.remove();
1✔
1041
            realm->commit_transaction();
1✔
1042
            audit->end_scope(scope, assert_no_error);
1✔
1043
            audit->wait_for_completion();
1✔
1044

1045
            auto events = get_audit_events(test_session);
1✔
1046
            REQUIRE(events.size() == 2);
1!
1047
            REQUIRE(events[0].event == "read");
1!
1048
            REQUIRE(events[1].event == "write");
1!
1049
            REQUIRE(events[0].data["value"][0]["_id"] == 2);
1!
1050
        }
1✔
1051

1052
        SECTION("reads after deletions report the correct object") {
2✔
1053
            auto scope = audit->begin_scope("scope");
1✔
1054
            realm->begin_transaction();
1✔
1055
            obj2.remove();
1✔
1056
            // In the pre-core-6 version of the code this would incorrectly
1057
            // report a read on obj2
1058
            Object(realm, obj3);
1✔
1059
            realm->commit_transaction();
1✔
1060
            audit->end_scope(scope, assert_no_error);
1✔
1061
            audit->wait_for_completion();
1✔
1062

1063
            auto events = get_audit_events(test_session);
1✔
1064
            REQUIRE(events.size() == 2);
1!
1065
            REQUIRE(events[0].event == "read");
1!
1066
            REQUIRE(events[1].event == "write");
1!
1067
            REQUIRE(events[0].data["value"][0]["_id"] == 3);
1!
1068
        }
1✔
1069
    }
2✔
1070
}
36✔
1071

1072
TEST_CASE("audit management", "[sync][pbs][audit]") {
15✔
1073
    TestClock clock;
15✔
1074

1075
    TestSyncManager test_session;
15✔
1076
    SyncTestFile config(test_session, "parent");
15✔
1077
    config.automatic_change_notifications = false;
15✔
1078
    config.schema_version = 1;
15✔
1079
    config.schema = Schema{
15✔
1080
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
15✔
1081
    };
15✔
1082
    config.audit_config = std::make_shared<AuditConfig>();
15✔
1083
    config.audit_config->base_file_path = test_session.base_file_path();
15✔
1084
    auto realm = Realm::get_shared_realm(config);
15✔
1085
    auto audit = realm->audit_context();
15✔
1086
    REQUIRE(audit);
15!
1087
    auto wait_for_completion = util::make_scope_exit([=]() noexcept {
15✔
1088
        audit->wait_for_completion();
15✔
1089
    });
15✔
1090

1091
    auto table = realm->read_group().get_table("class_object");
15✔
1092

1093
    // We open in proper sync mode to let the audit context initialize from that,
1094
    // but we don't actually want the realm to be synchronizing
1095
    realm->sync_session()->close();
15✔
1096

1097
    SECTION("config validation") {
15✔
1098
        SyncTestFile config(test_session, "parent2");
2✔
1099
        config.automatic_change_notifications = false;
2✔
1100
        config.audit_config = std::make_shared<AuditConfig>();
2✔
1101
        SECTION("invalid prefix") {
2✔
1102
            config.audit_config->partition_value_prefix = "";
1✔
1103
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidName,
1✔
1104
                              "Audit partition prefix must not be empty");
1✔
1105
            config.audit_config->partition_value_prefix = "/audit";
1✔
1106
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidName,
1✔
1107
                              "Invalid audit partition prefix '/audit': prefix must not contain slashes");
1✔
1108
        }
1✔
1109
        SECTION("invalid metadata") {
2✔
1110
            config.audit_config->metadata = {{"", "a"}};
1✔
1111
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidName,
1✔
1112
                              "Invalid audit metadata key '': keys must be 1-63 characters long");
1✔
1113
            std::string long_name(64, 'a');
1✔
1114
            config.audit_config->metadata = {{long_name, "b"}};
1✔
1115
            REQUIRE_EXCEPTION(
1✔
1116
                Realm::get_shared_realm(config), InvalidName,
1✔
1117
                "Invalid audit metadata key 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': keys "
1✔
1118
                "must be 1-63 characters long");
1✔
1119
            config.audit_config->metadata = {{"activity", "c"}};
1✔
1120
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidName,
1✔
1121
                              "Invalid audit metadata key 'activity': metadata keys cannot overlap with the audit "
1✔
1122
                              "event properties");
1✔
1123
            config.audit_config->metadata = {{"a", "d"}, {"a", "e"}};
1✔
1124
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidName, "Duplicate audit metadata key 'a'");
1✔
1125
        }
1✔
1126
    }
2✔
1127

1128
    SECTION("scope names") {
15✔
1129
        realm->begin_transaction();
1✔
1130
        auto obj = table->create_object_with_primary_key(1);
1✔
1131
        realm->commit_transaction();
1✔
1132

1133
        auto scope = audit->begin_scope("scope 1");
1✔
1134
        Object(realm, obj);
1✔
1135
        audit->end_scope(scope, assert_no_error);
1✔
1136

1137
        scope = audit->begin_scope("scope 2");
1✔
1138
        Object(realm, obj);
1✔
1139
        audit->end_scope(scope, assert_no_error);
1✔
1140
        audit->wait_for_completion();
1✔
1141

1142
        auto events = get_audit_events(test_session);
1✔
1143
        REQUIRE(events.size() == 2);
1!
1144
        REQUIRE(events[0].activity == "scope 1");
1!
1145
        REQUIRE(events[1].activity == "scope 2");
1!
1146
    }
1✔
1147

1148
    SECTION("nested scopes") {
15✔
1149
        realm->begin_transaction();
1✔
1150
        auto obj1 = table->create_object_with_primary_key(1);
1✔
1151
        auto obj2 = table->create_object_with_primary_key(2);
1✔
1152
        auto obj3 = table->create_object_with_primary_key(3);
1✔
1153
        realm->commit_transaction();
1✔
1154

1155
        auto scope1 = audit->begin_scope("scope 1");
1✔
1156
        Object(realm, obj1); // read in scope 1 only
1✔
1157

1158
        auto scope2 = audit->begin_scope("scope 2");
1✔
1159
        Object(realm, obj2); // read in both scopes
1✔
1160
        audit->end_scope(scope2, assert_no_error);
1✔
1161

1162
        Object(realm, obj3); // read in scope 1 only
1✔
1163

1164
        audit->end_scope(scope1, assert_no_error);
1✔
1165
        audit->wait_for_completion();
1✔
1166

1167
        auto events = get_audit_events(test_session);
1✔
1168
        REQUIRE(events.size() == 4);
1!
1169

1170
        // scope 2 read on obj 2 comes first as it was the first scope ended
1171
        REQUIRE(events[0].activity == "scope 2");
1!
1172
        REQUIRE(events[0].data["value"][0]["_id"] == 2);
1!
1173

1174
        // scope 1 then has reads on each object in order
1175
        REQUIRE(events[1].activity == "scope 1");
1!
1176
        REQUIRE(events[1].data["value"][0]["_id"] == 1);
1!
1177
        REQUIRE(events[2].activity == "scope 1");
1!
1178
        REQUIRE(events[2].data["value"][0]["_id"] == 2);
1!
1179
        REQUIRE(events[3].activity == "scope 1");
1!
1180
        REQUIRE(events[3].data["value"][0]["_id"] == 3);
1!
1181
    }
1✔
1182

1183
    SECTION("overlapping scopes") {
15✔
1184
        realm->begin_transaction();
1✔
1185
        auto obj1 = table->create_object_with_primary_key(1);
1✔
1186
        auto obj2 = table->create_object_with_primary_key(2);
1✔
1187
        auto obj3 = table->create_object_with_primary_key(3);
1✔
1188
        realm->commit_transaction();
1✔
1189

1190
        auto scope1 = audit->begin_scope("scope 1");
1✔
1191
        Object(realm, obj1); // read in scope 1 only
1✔
1192

1193
        auto scope2 = audit->begin_scope("scope 2");
1✔
1194
        Object(realm, obj2); // read in both scopes
1✔
1195

1196
        audit->end_scope(scope1, assert_no_error);
1✔
1197
        Object(realm, obj3); // read in scope 2 only
1✔
1198

1199
        audit->end_scope(scope2, assert_no_error);
1✔
1200
        audit->wait_for_completion();
1✔
1201

1202
        auto events = get_audit_events(test_session);
1✔
1203
        REQUIRE(events.size() == 4);
1!
1204

1205
        // scope 1 only read on obj 1
1206
        REQUIRE(events[0].activity == "scope 1");
1!
1207
        REQUIRE(events[0].data["value"][0]["_id"] == 1);
1!
1208

1209
        // both scopes read on obj 2
1210
        REQUIRE(events[1].activity == "scope 1");
1!
1211
        REQUIRE(events[1].data["value"][0]["_id"] == 2);
1!
1212
        REQUIRE(events[2].activity == "scope 2");
1!
1213
        REQUIRE(events[2].data["value"][0]["_id"] == 2);
1!
1214

1215
        // scope 2 only read on obj 3
1216
        REQUIRE(events[3].activity == "scope 2");
1!
1217
        REQUIRE(events[3].data["value"][0]["_id"] == 3);
1!
1218
    }
1✔
1219

1220
    SECTION("scope cancellation") {
15✔
1221
        realm->begin_transaction();
1✔
1222
        auto obj = table->create_object_with_primary_key(1);
1✔
1223
        realm->commit_transaction();
1✔
1224

1225
        auto scope1 = audit->begin_scope("scope 1");
1✔
1226
        auto scope2 = audit->begin_scope("scope 2");
1✔
1227
        Object(realm, obj);
1✔
1228
        audit->cancel_scope(scope1);
1✔
1229
        audit->end_scope(scope2, assert_no_error);
1✔
1230
        audit->wait_for_completion();
1✔
1231

1232
        auto events = get_audit_events(test_session);
1✔
1233
        REQUIRE(events.size() == 1);
1!
1234
        REQUIRE(events[0].activity == "scope 2");
1!
1235
    }
1✔
1236

1237
    SECTION("ending invalid scopes") {
15✔
1238
        REQUIRE_FALSE(audit->is_scope_valid(0));
1!
1239
        REQUIRE_THROWS_WITH(audit->end_scope(0),
1✔
1240
                            "Cannot end event scope: scope '0' not in progress. Scope may have already been ended?");
1✔
1241

1242
        auto scope = audit->begin_scope("scope");
1✔
1243
        REQUIRE(audit->is_scope_valid(scope));
1!
1244
        REQUIRE_NOTHROW(audit->end_scope(scope));
1✔
1245

1246
        REQUIRE_FALSE(audit->is_scope_valid(scope));
1!
1247
        REQUIRE_THROWS_WITH(audit->end_scope(scope),
1✔
1248
                            "Cannot end event scope: scope '1' not in progress. Scope may have already been ended?");
1✔
1249

1250
        scope = audit->begin_scope("scope 2");
1✔
1251
        REQUIRE(audit->is_scope_valid(scope));
1!
1252
        REQUIRE_NOTHROW(audit->cancel_scope(scope));
1✔
1253

1254
        REQUIRE_FALSE(audit->is_scope_valid(scope));
1!
1255
        REQUIRE_THROWS_WITH(audit->cancel_scope(scope),
1✔
1256
                            "Cannot end event scope: scope '2' not in progress. Scope may have already been ended?");
1✔
1257
    }
1✔
1258

1259
    SECTION("event timestamps") {
15✔
1260
        std::vector<Obj> objects;
1✔
1261
        realm->begin_transaction();
1✔
1262
        for (int i = 0; i < 10; ++i)
11✔
1263
            objects.push_back(table->create_object_with_primary_key(i));
10✔
1264
        realm->commit_transaction();
1✔
1265

1266
        auto scope = audit->begin_scope("scope");
1✔
1267
        for (int i = 0; i < 10; ++i) {
11✔
1268
            Object(realm, objects[i]);
10✔
1269
            Object(realm, objects[i]);
10✔
1270
        }
10✔
1271
        audit->end_scope(scope, assert_no_error);
1✔
1272
        audit->wait_for_completion();
1✔
1273

1274
        auto events = get_audit_events(test_session);
1✔
1275
        REQUIRE(events.size() == 10);
1!
1276
        for (int i = 0; i < 10; ++i) {
11✔
1277
            // i * 2 because we generate two reads on each object and the second
1278
            // is dropped, but still should have called now().
1279
            REQUIRE(events[i].timestamp == Timestamp(1000 + i * 2, 1000 + i * 2));
10!
1280
        }
10✔
1281
    }
1✔
1282

1283
    SECTION("metadata updating") {
15✔
1284
        realm->begin_transaction();
5✔
1285
        auto obj1 = realm->read_group().get_table("class_object")->create_object_with_primary_key(1);
5✔
1286
        realm->read_group().get_table("class_object")->create_object_with_primary_key(2);
5✔
1287
        realm->read_group().get_table("class_object")->create_object_with_primary_key(3);
5✔
1288
        realm->commit_transaction();
5✔
1289

1290
        SECTION("update before scope") {
5✔
1291
            audit->update_metadata({{"a", "aa"}});
1✔
1292
            auto scope = audit->begin_scope("scope 1");
1✔
1293
            Object(realm, obj1);
1✔
1294
            audit->end_scope(scope, assert_no_error);
1✔
1295
            audit->wait_for_completion();
1✔
1296

1297
            auto events = get_audit_events(test_session);
1✔
1298
            REQUIRE(events.size() == 1);
1!
1299
            auto event = events[0];
1✔
1300
            REQUIRE(event.metadata.size() == 1);
1!
1301
            REQUIRE(event.metadata["a"] == "aa");
1!
1302
        }
1✔
1303

1304
        SECTION("update during scope") {
5✔
1305
            auto scope = audit->begin_scope("scope 1");
1✔
1306
            audit->update_metadata({{"a", "aa"}});
1✔
1307
            Object(realm, obj1);
1✔
1308
            audit->end_scope(scope, assert_no_error);
1✔
1309
            audit->wait_for_completion();
1✔
1310

1311
            auto events = get_audit_events(test_session);
1✔
1312
            REQUIRE(events.size() == 1);
1!
1313
            auto event = events[0];
1✔
1314
            REQUIRE(event.metadata.size() == 0);
1!
1315
        }
1✔
1316

1317
        SECTION("one metadata field at a time") {
5✔
1318
            for (int i = 0; i < 100; ++i) {
101✔
1319
                audit->update_metadata({{util::format("name %1", i), util::format("value %1", i)}});
100✔
1320
                auto scope = audit->begin_scope(util::format("scope %1", i));
100✔
1321
                Object(realm, obj1);
100✔
1322
                audit->end_scope(scope, assert_no_error);
100✔
1323
            }
100✔
1324
            audit->wait_for_completion();
1✔
1325

1326
            auto events = get_audit_events(test_session);
1✔
1327
            REQUIRE(events.size() == 100);
1!
1328
            for (size_t i = 0; i < 100; ++i) {
101✔
1329
                REQUIRE(events[i].metadata.size() == 1);
100!
1330
                REQUIRE(events[i].metadata[util::format("name %1", i)] == util::format("value %1", i));
100!
1331
            }
100✔
1332
        }
1✔
1333

1334
        SECTION("many metadata fields") {
5✔
1335
            std::vector<std::pair<std::string, std::string>> metadata;
1✔
1336
            for (int i = 0; i < 100; ++i) {
101✔
1337
                metadata.push_back({util::format("name %1", i), util::format("value %1", i)});
100✔
1338
                audit->update_metadata(std::vector(metadata));
100✔
1339
                auto scope = audit->begin_scope(util::format("scope %1", i));
100✔
1340
                Object(realm, obj1);
100✔
1341
                audit->end_scope(scope, assert_no_error);
100✔
1342
            }
100✔
1343
            audit->wait_for_completion();
1✔
1344

1345
            auto events = get_audit_events(test_session);
1✔
1346
            REQUIRE(events.size() == 100);
1!
1347
            for (size_t i = 0; i < 100; ++i) {
101✔
1348
                REQUIRE(events[i].metadata.size() == i + 1);
100!
1349
            }
100✔
1350
        }
1✔
1351

1352
        SECTION("update via opening new realm") {
5✔
1353
            config.audit_config->metadata = {{"a", "aa"}};
1✔
1354
            auto realm2 = Realm::get_shared_realm(config);
1✔
1355
            auto obj2 = realm2->read_group().get_table("class_object")->get_object(1);
1✔
1356

1357
            auto scope = audit->begin_scope("scope 1");
1✔
1358
            Object(realm, obj1);
1✔
1359
            Object(realm2, obj2);
1✔
1360
            audit->end_scope(scope, assert_no_error);
1✔
1361

1362
            config.audit_config->metadata = {{"a", "aaa"}, {"b", "bb"}};
1✔
1363
            auto realm3 = Realm::get_shared_realm(config);
1✔
1364
            auto obj3 = realm3->read_group().get_table("class_object")->get_object(2);
1✔
1365

1366
            scope = audit->begin_scope("scope 2");
1✔
1367
            Object(realm, obj1);
1✔
1368
            Object(realm2, obj2);
1✔
1369
            Object(realm3, obj3);
1✔
1370
            audit->end_scope(scope, assert_no_error);
1✔
1371
            audit->wait_for_completion();
1✔
1372

1373
            auto events = get_audit_events(test_session);
1✔
1374
            REQUIRE(events.size() == 5);
1!
1375
            REQUIRE(events[0].activity == "scope 1");
1!
1376
            REQUIRE(events[1].activity == "scope 1");
1!
1377
            REQUIRE(events[2].activity == "scope 2");
1!
1378
            REQUIRE(events[3].activity == "scope 2");
1!
1379
            REQUIRE(events[4].activity == "scope 2");
1!
1380
            REQUIRE(events[0].metadata.size() == 1);
1!
1381
            REQUIRE(events[1].metadata.size() == 1);
1!
1382
            REQUIRE(events[2].metadata.size() == 2);
1!
1383
            REQUIRE(events[3].metadata.size() == 2);
1!
1384
            REQUIRE(events[4].metadata.size() == 2);
1!
1385
        }
1✔
1386
    }
5✔
1387

1388
    SECTION("custom audit event") {
15✔
1389
        // Verify that each of the completion handlers is called in the expected order
1390
        std::atomic<size_t> completions = 0;
1✔
1391
        std::array<std::pair<std::atomic<size_t>, std::atomic<bool>>, 5> completion_results;
1✔
1392
        auto expect_completion = [&](size_t expected) {
5✔
1393
            return [&, expected](std::exception_ptr e) {
5✔
1394
                completion_results[expected].second = bool(e);
5✔
1395
                completion_results[expected].first = completions++;
5✔
1396
            };
5✔
1397
        };
5✔
1398

1399
        audit->record_event("event 1", "event"s, "data"s, expect_completion(0));
1✔
1400
        audit->record_event("event 2", none, "data"s, expect_completion(1));
1✔
1401
        auto scope = audit->begin_scope("scope");
1✔
1402
        // note: does not use the scope's activity
1403
        audit->record_event("event 3", none, none, expect_completion(2));
1✔
1404
        audit->end_scope(scope, expect_completion(3));
1✔
1405
        audit->record_event("event 4", none, none, expect_completion(4));
1✔
1406

1407
        util::EventLoop::main().run_until([&] {
162✔
1408
            return completions == 5;
162✔
1409
        });
162✔
1410

1411
        for (size_t i = 0; i < 5; ++i) {
6✔
1412
            REQUIRE(i == completion_results[i].first);
5!
1413
            REQUIRE_FALSE(completion_results[i].second);
5!
1414
        }
5✔
1415

1416
        auto events = get_audit_events(test_session, false);
1✔
1417
        REQUIRE(events.size() == 4);
1!
1418
        REQUIRE(events[0].activity == "event 1");
1!
1419
        REQUIRE(events[1].activity == "event 2");
1!
1420
        REQUIRE(events[2].activity == "event 3");
1!
1421
        REQUIRE(events[3].activity == "event 4");
1!
1422
        REQUIRE(events[0].event == "event"s);
1!
1423
        REQUIRE(events[1].event == none);
1!
1424
        REQUIRE(events[2].event == none);
1!
1425
        REQUIRE(events[3].event == none);
1!
1426
        REQUIRE(events[0].raw_data == "data"s);
1!
1427
        REQUIRE(events[1].raw_data == "data"s);
1!
1428
        REQUIRE(events[2].raw_data == none);
1!
1429
        REQUIRE(events[3].raw_data == none);
1!
1430
    }
1✔
1431

1432
    SECTION("read transaction version management") {
15✔
1433
        realm->begin_transaction();
1✔
1434
        auto obj = table->create_object_with_primary_key(1);
1✔
1435
        realm->commit_transaction();
1✔
1436

1437
        auto realm2 = Realm::get_shared_realm(config);
1✔
1438
        auto obj2 = realm2->read_group().get_table("class_object")->get_object(0);
1✔
1439
        auto realm3 = Realm::get_shared_realm(config);
1✔
1440
        auto obj3 = realm3->read_group().get_table("class_object")->get_object(0);
1✔
1441

1442
        realm2->begin_transaction();
1✔
1443
        obj2.set_all(1);
1✔
1444
        realm2->commit_transaction();
1✔
1445

1446
        realm3->begin_transaction();
1✔
1447
        obj3.set_all(2);
1✔
1448
        realm3->commit_transaction();
1✔
1449

1450
        auto scope = audit->begin_scope("scope");
1✔
1451
        Object(realm3, obj3); // value 2
1✔
1452
        Object(realm2, obj2); // value 1
1✔
1453
        Object(realm, obj);   // value 0
1✔
1454
        realm->refresh();
1✔
1455
        Object(realm, obj);   // value 2
1✔
1456
        Object(realm2, obj2); // value 1
1✔
1457
        realm2->refresh();
1✔
1458
        Object(realm3, obj3); // value 2
1✔
1459
        Object(realm2, obj2); // value 2
1✔
1460
        Object(realm, obj);   // value 2
1✔
1461
        audit->end_scope(scope, assert_no_error);
1✔
1462
        audit->wait_for_completion();
1✔
1463

1464
        auto events = get_audit_events(test_session);
1✔
1465
        INFO(events);
1✔
1466
        REQUIRE(events.size() == 6);
1!
1467
        std::string str = events[0].data.dump();
1✔
1468
        // initial
1469
        REQUIRE(events[0].data["value"][0]["value"] == 2);
1!
1470
        REQUIRE(events[1].data["value"][0]["value"] == 1);
1!
1471
        REQUIRE(events[2].data["value"][0]["value"] == 0);
1!
1472

1473
        // realm->refresh()
1474
        REQUIRE(events[3].data["value"][0]["value"] == 2);
1!
1475
        REQUIRE(events[4].data["value"][0]["value"] == 1);
1!
1476

1477
        // realm2->refresh()
1478
        REQUIRE(events[5].data["value"][0]["value"] == 2);
1!
1479
    }
1✔
1480

1481
#if !REALM_DEBUG // This test is unreasonably slow in debug mode
1482
    SECTION("large audit scope") {
1483
        realm->begin_transaction();
1484
        auto obj1 = table->create_object_with_primary_key(1);
1485
        auto obj2 = table->create_object_with_primary_key(2);
1486
        realm->commit_transaction();
1487

1488
        auto scope = audit->begin_scope("large");
1489
        for (int i = 0; i < 150'000; ++i) {
1490
            Object(realm, obj1);
1491
            Object(realm, obj2);
1492
        }
1493
        audit->end_scope(scope, assert_no_error);
1494
        audit->wait_for_completion();
1495

1496
        auto events = get_audit_events(test_session);
1497
        REQUIRE(events.size() == 300'000);
1498
    }
1499
#endif
1500
}
15✔
1501

1502
TEST_CASE("audit realm sharding", "[sync][pbs][audit]") {
3✔
1503
    // Don't start the server immediately so that we're forced to accumulate
1504
    // a lot of local unuploaded data.
1505
    TestSyncManager test_session{{}, {.start_immediately = false}};
3✔
1506

1507
    SyncTestFile config(test_session, "parent");
3✔
1508
    config.automatic_change_notifications = false;
3✔
1509
    config.schema_version = 1;
3✔
1510
    config.schema = Schema{
3✔
1511
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
3✔
1512
    };
3✔
1513
    config.audit_config = std::make_shared<AuditConfig>();
3✔
1514
    config.audit_config->base_file_path = test_session.base_file_path();
3✔
1515
    config.audit_config->logger = audit_logger;
3✔
1516
    auto realm = Realm::get_shared_realm(config);
3✔
1517
    auto audit = realm->audit_context();
3✔
1518
    REQUIRE(audit);
3!
1519

1520
    auto table = realm->read_group().get_table("class_object");
3✔
1521

1522
    // We open in proper sync mode to let the audit context initialize from that,
1523
    // but we don't actually want the realm to be synchronizing
1524
    realm->sync_session()->close();
3✔
1525

1526
    // Set a small shard size so that we don't have to write an absurd
1527
    // amount of data to test this
1528
    audit_test_hooks::set_maximum_shard_size(32 * 1024);
3✔
1529
    auto cleanup = util::make_scope_exit([]() noexcept {
3✔
1530
        audit_test_hooks::set_maximum_shard_size(256 * 1024 * 1024);
3✔
1531
    });
3✔
1532

1533
    realm->begin_transaction();
3✔
1534
    std::vector<Obj> objects;
3✔
1535
    for (int i = 0; i < 2000; ++i)
6,003✔
1536
        objects.push_back(table->create_object_with_primary_key(i));
6,000✔
1537
    realm->commit_transaction();
3✔
1538

1539
    // Write a lot of audit scopes while unable to sync
1540
    for (int i = 0; i < 50; ++i) {
153✔
1541
        auto scope = audit->begin_scope(util::format("scope %1", i));
150✔
1542
        Results(realm, table->where()).snapshot();
150✔
1543
        audit->end_scope(scope, assert_no_error);
150✔
1544
    }
150✔
1545
    audit->wait_for_completion();
3✔
1546

1547
    // There should now be several unuploaded Realms in the local client
1548
    // directory
1549
    auto root = test_session.base_file_path() + "/realm-audit/app id/test/audit";
3✔
1550
    std::string file_name;
3✔
1551
    util::DirScanner dir(root);
3✔
1552
    size_t file_count = 0;
3✔
1553
    std::vector<std::string> unlocked_files;
3✔
1554
    while (dir.next(file_name)) {
84✔
1555
        if (!StringData(file_name).ends_with(".realm"))
81✔
1556
            continue;
54✔
1557
        ++file_count;
27✔
1558
        // The upper limit is a soft cap, so files might be a bit bigger
1559
        // than it. 1 MB errs on the side of never getting spurious failures.
1560
        REQUIRE(util::File::get_size_static(root + "/" + file_name) < 1024 * 1024);
27!
1561
        if (DB::call_with_lock(root + "/" + file_name, [](auto&) {})) {
27✔
1562
            unlocked_files.push_back(file_name);
21✔
1563
        }
21✔
1564
    }
27✔
1565
    // The exact number of shards is fuzzy due to the combination of the
1566
    // soft cap on size and the fact that changesets are compressed, but
1567
    // there definitely should be more than one.
1568
    REQUIRE(file_count > 2);
3!
1569
    // There should be exactly two files open still: the one we're currently
1570
    // writing to, and the first one which we wrote and are waiting for the
1571
    // upload to complete.
1572
    REQUIRE(unlocked_files.size() == file_count - 2);
3!
1573

1574
    // Create a backup copy of each of the unlocked files which should be cleaned up
1575
    for (auto& file : unlocked_files) {
21✔
1576
        BackupHandler handler(root + "/" + file, {}, {});
21✔
1577
        handler.backup_realm_if_needed(23, 24);
21✔
1578
        // Set the version field in the backup file to 23 so that opening it
1579
        // won't accidentally work
1580
        util::File(handler.get_prefix() + "v23.backup.realm", util::File::mode_Update).write(12, "\x17");
21✔
1581
    }
21✔
1582

1583
    auto get_sorted_events = [&] {
3✔
1584
        auto events = get_audit_events(test_session, false);
3✔
1585
        // The events might be out of order because there's no guaranteed order
1586
        // for both uploading the Realms and for opening the uploaded Realms.
1587
        // Once sorted by timestamp the scopes should be in order, though.
1588
        sort_events(events);
3✔
1589
        return events;
3✔
1590
    };
3✔
1591
    auto close_all_sessions = [&] {
3✔
1592
        realm->close();
2✔
1593
        realm = nullptr;
2✔
1594
        auto sync_manager = test_session.sync_manager();
2✔
1595
        for (auto& session : sync_manager->get_all_sessions()) {
2✔
1596
            session->shutdown_and_wait();
2✔
1597
        }
2✔
1598
    };
2✔
1599

1600
    SECTION("start server with existing session open") {
3✔
1601
        test_session.sync_server().start();
1✔
1602
        audit->wait_for_uploads();
1✔
1603

1604
        auto events = get_sorted_events();
1✔
1605
        REQUIRE(events.size() == 50);
1!
1606
        for (int i = 0; i < 50; ++i) {
51✔
1607
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
1608
        }
50✔
1609

1610
        // There should be exactly one remaining local Realm file (the currently
1611
        // open one that hasn't hit the size limit yet)
1612
        size_t remaining_realms = 0;
1✔
1613
        util::DirScanner dir(root);
1✔
1614
        while (dir.next(file_name)) {
27✔
1615
            if (StringData(file_name).ends_with(".realm"))
26✔
1616
                ++remaining_realms;
1✔
1617
        }
26✔
1618
        REQUIRE(remaining_realms == 1);
1!
1619
    }
1✔
1620

1621
    SECTION("trigger uploading by opening a new Realm") {
3✔
1622
        close_all_sessions();
1✔
1623
        test_session.sync_server().start();
1✔
1624

1625
        // Open a different Realm with the same user and audit prefix
1626
        SyncTestFile config(test_session, "other");
1✔
1627
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1628
        config.audit_config->logger = audit_logger;
1✔
1629
        config.audit_config->base_file_path = test_session.base_file_path();
1✔
1630
        auto realm = Realm::get_shared_realm(config);
1✔
1631
        auto audit2 = realm->audit_context();
1✔
1632
        REQUIRE(audit2);
1!
1633
        audit2->wait_for_uploads();
1✔
1634

1635
        auto events = get_sorted_events();
1✔
1636
        REQUIRE(events.size() == 50);
1!
1637
        for (int i = 0; i < 50; ++i) {
51✔
1638
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
1639
        }
50✔
1640

1641
        // There should be no remaining local Realm files because we haven't
1642
        // made the new audit context open a Realm yet
1643
        util::DirScanner dir(root);
1✔
1644
        while (dir.next(file_name)) {
26✔
1645
            REQUIRE_FALSE(StringData(file_name).ends_with(".realm"));
25!
1646
        }
25✔
1647
    }
1✔
1648

1649
    SECTION("uploading is per audit prefix") {
3✔
1650
        close_all_sessions();
1✔
1651
        test_session.sync_server().start();
1✔
1652

1653
        // Open the same Realm with a different audit prefix
1654
        SyncTestFile config(test_session, "parent");
1✔
1655
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1656
        config.audit_config->base_file_path = test_session.base_file_path();
1✔
1657
        config.audit_config->logger = audit_logger;
1✔
1658
        config.audit_config->partition_value_prefix = "other";
1✔
1659
        auto realm = Realm::get_shared_realm(config);
1✔
1660
        auto audit2 = realm->audit_context();
1✔
1661
        REQUIRE(audit2);
1!
1662
        audit2->wait_for_uploads();
1✔
1663

1664
        // Should not have uploaded any of the old events
1665
        auto events = get_sorted_events();
1✔
1666
        REQUIRE(events.size() == 0);
1!
1667
    }
1✔
1668
}
3✔
1669

1670
#if REALM_ENABLE_AUTH_TESTS
1671
static void generate_event(std::shared_ptr<Realm> realm, int call = 0)
1672
{
32✔
1673
    auto table = realm->read_group().get_table("class_object");
32✔
1674
    auto audit = realm->audit_context();
32✔
1675

1676
    realm->begin_transaction();
32✔
1677
    table->create_object_with_primary_key(call + 1).set_all(2);
32✔
1678
    realm->commit_transaction();
32✔
1679

1680
    auto scope = audit->begin_scope("scope");
32✔
1681
    Object(realm, table->get_object(call));
32✔
1682
    audit->end_scope(scope, assert_no_error);
32✔
1683
}
32✔
1684

1685
TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") {
14✔
1686
    // None of these tests need a deterministic clock, but the server rounding
1687
    // timestamps to milliseconds can result in events not having monotonically
1688
    // increasing timestamps with an actual clock.
1689
    TestClock clock;
14✔
1690

1691
    const Schema schema{
14✔
1692
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
14✔
1693
        {"AuditEvent",
14✔
1694
         {
14✔
1695
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
14✔
1696
             {"timestamp", PropertyType::Date},
14✔
1697
             {"activity", PropertyType::String},
14✔
1698
             {"event", PropertyType::String | PropertyType::Nullable},
14✔
1699
             {"data", PropertyType::String | PropertyType::Nullable},
14✔
1700
             {"metadata 1", PropertyType::String | PropertyType::Nullable},
14✔
1701
             {"metadata 2", PropertyType::String | PropertyType::Nullable},
14✔
1702
         }}};
14✔
1703
    const Schema no_audit_event_schema{
14✔
1704
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}};
14✔
1705

1706
    auto app_create_config = default_app_config();
14✔
1707
    app_create_config.schema = schema;
14✔
1708
    app_create_config.dev_mode_enabled = false;
14✔
1709
    TestAppSession session = create_app(app_create_config);
14✔
1710

1711
    SyncTestFile config(session.app()->current_user(), bson::Bson("default"));
14✔
1712
    config.automatic_change_notifications = false;
14✔
1713
    config.schema = schema;
14✔
1714
    config.audit_config = std::make_shared<AuditConfig>();
14✔
1715
    config.audit_config->logger = audit_logger;
14✔
1716
    config.audit_config->base_file_path = session.app()->config().base_file_path;
14✔
1717

1718
    auto expect_error = [&](auto&& config, auto&& fn) -> SyncError {
14✔
1719
        std::mutex mutex;
3✔
1720
        util::Optional<SyncError> error;
3✔
1721
        config.audit_config->sync_error_handler = [&](SyncError e) {
3✔
1722
            std::lock_guard lock(mutex);
3✔
1723
            error = e;
3✔
1724
        };
3✔
1725

1726
        auto realm = Realm::get_shared_realm(config);
3✔
1727
        fn(realm, 0);
3✔
1728

1729
        timed_wait_for(
3✔
1730
            [&] {
1,694✔
1731
                std::lock_guard lock(mutex);
1,694✔
1732
                return (bool)error;
1,694✔
1733
            },
1,694✔
1734
            std::chrono::seconds(30));
3✔
1735
        REQUIRE(bool(error));
3!
1736
        return *error;
3✔
1737
    };
3✔
1738

1739
    SECTION("basic functionality") {
14✔
1740
        auto realm = Realm::get_shared_realm(config);
1✔
1741
        realm->sync_session()->close();
1✔
1742
        generate_event(realm);
1✔
1743

1744
        auto events = get_audit_events_from_baas(session, *session.app()->current_user(), 1);
1✔
1745
        REQUIRE(events.size() == 1);
1!
1746
        REQUIRE(events[0].activity == "scope");
1!
1747
        REQUIRE(events[0].event == "read");
1!
1748
        REQUIRE(!events[0].timestamp.is_null()); // FIXME
1!
1749
        REQUIRE(events[0].data == json({{"type", "object"}, {"value", {{{"_id", 1}, {"value", 2}}}}}));
1!
1750
    }
1✔
1751

1752
    SECTION("different user from parent Realm") {
14✔
1753
        auto sync_user = session.app()->current_user();
1✔
1754
        create_user_and_log_in(session.app());
1✔
1755
        auto audit_user = session.app()->current_user();
1✔
1756
        config.audit_config->audit_user = audit_user;
1✔
1757
        auto realm = Realm::get_shared_realm(config);
1✔
1758
        // If audit uses the sync user this'll make it fail as that user is logged out
1759
        sync_user->log_out();
1✔
1760

1761
        generate_event(realm);
1✔
1762
        REQUIRE(get_audit_events_from_baas(session, *audit_user, 1).size() == 1);
1!
1763
    }
1✔
1764

1765
    SECTION("different app from parent Realm") {
14✔
1766
        auto audit_user = session.app()->current_user();
1✔
1767

1768
        // Create an app which does not include AuditEvent in the schema so that
1769
        // things will break if audit tries to use it
1770
        app_create_config.schema = no_audit_event_schema;
1✔
1771
        TestAppSession session_2 = create_app(app_create_config);
1✔
1772
        SyncTestFile config(session_2.app()->current_user(), bson::Bson("default"));
1✔
1773
        config.schema = no_audit_event_schema;
1✔
1774
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1775
        config.audit_config->base_file_path = session.app()->config().base_file_path;
1✔
1776
        config.audit_config->audit_user = audit_user;
1✔
1777

1778
        auto realm = Realm::get_shared_realm(config);
1✔
1779
        generate_event(realm);
1✔
1780
        REQUIRE(get_audit_events_from_baas(session, *audit_user, 1).size() == 1);
1!
1781
    }
1✔
1782

1783
    SECTION("valid metadata properties") {
14✔
1784
        auto realm = Realm::get_shared_realm(config);
1✔
1785
        generate_event(realm, 0);
1✔
1786
        realm->audit_context()->update_metadata({{"metadata 1", "value 1"}});
1✔
1787
        generate_event(realm, 1);
1✔
1788
        realm->audit_context()->update_metadata({{"metadata 2", "value 2"}});
1✔
1789
        generate_event(realm, 2);
1✔
1790
        realm->audit_context()->update_metadata({{"metadata 1", "value 3"}, {"metadata 2", "value 4"}});
1✔
1791
        generate_event(realm, 3);
1✔
1792

1793
        using Metadata = std::map<std::string, std::string>;
1✔
1794
        auto events = get_audit_events_from_baas(session, *session.app()->current_user(), 4);
1✔
1795
        REQUIRE(events[0].metadata.empty());
1!
1796
        REQUIRE(events[1].metadata == Metadata({{"metadata 1", "value 1"}}));
1!
1797
        REQUIRE(events[2].metadata == Metadata({{"metadata 2", "value 2"}}));
1!
1798
        REQUIRE(events[3].metadata == Metadata({{"metadata 1", "value 3"}, {"metadata 2", "value 4"}}));
1!
1799
    }
1✔
1800

1801
    SECTION("invalid metadata properties") {
14✔
1802
        config.audit_config->metadata = {{"invalid key", "value"}};
1✔
1803
        auto error = expect_error(config, generate_event);
1✔
1804
        REQUIRE_THAT(error.status.reason(), StartsWith("Invalid schema change"));
1✔
1805
        REQUIRE(error.is_fatal);
1!
1806
    }
1✔
1807

1808
    SECTION("removed sync user") {
14✔
1809
        create_user_and_log_in(session.app());
1✔
1810
        auto audit_user = session.app()->current_user();
1✔
1811
        config.audit_config->audit_user = audit_user;
1✔
1812
        auto realm = Realm::get_shared_realm(config);
1✔
1813
        session.app()->remove_user(audit_user, nullptr);
1✔
1814

1815
        auto audit = realm->audit_context();
1✔
1816
        auto scope = audit->begin_scope("scope");
1✔
1817
        realm->begin_transaction();
1✔
1818
        auto table = realm->read_group().get_table("class_object");
1✔
1819
        table->create_object_with_primary_key(1).set_all(2);
1✔
1820
        realm->commit_transaction();
1✔
1821

1822
        audit->end_scope(scope, [&](auto error) {
1✔
1823
            REQUIRE(error);
1!
1824
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "user has been removed");
1✔
1825
        });
1✔
1826
        audit->wait_for_completion();
1✔
1827
    }
1✔
1828

1829
    SECTION("AuditEvent missing from server schema") {
14✔
1830
        app_create_config.schema = no_audit_event_schema;
1✔
1831
        TestAppSession session_2 = create_app(app_create_config);
1✔
1832
        SyncTestFile config(session_2.app()->current_user(), bson::Bson("default"));
1✔
1833
        config.schema = no_audit_event_schema;
1✔
1834
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1835
        config.audit_config->base_file_path = session.app()->config().base_file_path;
1✔
1836

1837
        auto error = expect_error(config, generate_event);
1✔
1838
        REQUIRE_THAT(error.status.reason(), StartsWith("Invalid schema change"));
1✔
1839
        REQUIRE(error.is_fatal);
1!
1840
    }
1✔
1841

1842
    SECTION("incoming changesets are discarded") {
14✔
1843
        app::MongoClient remote_client = session.app()->current_user()->mongo_client("BackingDB");
2✔
1844
        app::MongoDatabase db = remote_client.db(session.app_session().config.mongo_dbname);
2✔
1845
        app::MongoCollection collection = db["AuditEvent"];
2✔
1846

1847
        SECTION("objects deleted on server") {
2✔
1848
            // Because EraseObject is idempotent, this case actually just works
1849
            // without any special logic.
1850
            auto delete_one = [&] {
10✔
1851
                uint64_t deleted = 0;
10✔
1852
                while (deleted == 0) {
90✔
1853
                    collection.delete_one({},
80✔
1854
                                          [&](util::Optional<uint64_t>&& count, util::Optional<app::AppError> error) {
80✔
1855
                                              REQUIRE_FALSE(error);
80!
1856
                                              deleted = *count;
80✔
1857
                                          });
80✔
1858
                    if (deleted == 0) {
80✔
1859
                        millisleep(100); // slow down the number of retries
70✔
1860
                    }
70✔
1861
                }
80✔
1862
            };
10✔
1863

1864
            auto realm = Realm::get_shared_realm(config);
1✔
1865
            for (int i = 0; i < 10; ++i) {
11✔
1866
                generate_event(realm, i);
10✔
1867
                delete_one();
10✔
1868
            }
10✔
1869
        }
1✔
1870

1871
        SECTION("objects modified on server") {
2✔
1872
            // UpdateObject throws bad_transaction_log() if the object doesn't
1873
            // exist locally, so this will break if we try to apply the changesets
1874
            // from the server.
1875
            const bson::BsonDocument filter{{"event", "read"}};
1✔
1876
            const bson::BsonDocument update{{"$set", bson::BsonDocument{{"event", "processed"}}}};
1✔
1877
            auto update_one = [&] {
10✔
1878
                int32_t count = 0;
10✔
1879
                while (count == 0) {
95✔
1880
                    collection.update_one(
85✔
1881
                        filter, update,
85✔
1882
                        [&](app::MongoCollection::UpdateResult result, util::Optional<app::AppError> error) {
85✔
1883
                            REQUIRE_FALSE(error);
85!
1884
                            count = result.modified_count;
85✔
1885
                        });
85✔
1886
                    if (count == 0) {
85✔
1887
                        millisleep(100); // slow down the number of retries
75✔
1888
                    }
75✔
1889
                }
85✔
1890
            };
10✔
1891

1892
            auto realm = Realm::get_shared_realm(config);
1✔
1893
            for (int i = 0; i < 10; ++i) {
11✔
1894
                generate_event(realm, i);
10✔
1895
                update_one();
10✔
1896
            }
10✔
1897
        }
1✔
1898
    }
2✔
1899

1900
    SECTION("flexible sync") {
14✔
1901
        app::FLXSyncTestHarness harness("audit", {schema});
3✔
1902
        create_user_and_log_in(harness.app());
3✔
1903

1904
        SECTION("auditing a flexible sync realm without specifying an audit user throws an exception") {
3✔
1905
            SyncTestFile config(harness.app()->current_user(), schema, SyncConfig::FLXSyncEnabled{});
1✔
1906
            config.audit_config = std::make_shared<AuditConfig>();
1✔
1907
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "partition-based sync");
1✔
1908
        }
1✔
1909

1910
        SECTION("auditing with a flexible sync user reports a sync error") {
3✔
1911
            config.audit_config->audit_user = harness.app()->current_user();
1✔
1912
            auto error = expect_error(config, generate_event);
1✔
1913
            REQUIRE_THAT(error.status.reason(),
1✔
1914
                         Catch::Matchers::ContainsSubstring(
1✔
1915
                             "Client connected using partition-based sync when app is using flexible sync"));
1✔
1916
            REQUIRE(error.is_fatal);
1!
1917
        }
1✔
1918

1919
        SECTION("auditing a flexible sync realm with a pbs audit user works") {
3✔
1920
            config.audit_config->audit_user = config.sync_config->user;
1✔
1921
            config.sync_config->user = harness.app()->current_user();
1✔
1922
            config.sync_config->flx_sync_requested = true;
1✔
1923
            config.sync_config->partition_value.clear();
1✔
1924
            config.schema_version = 0;
1✔
1925

1926
            auto realm = Realm::get_shared_realm(config);
1✔
1927
            {
1✔
1928
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
1✔
1929
                mut_subs.insert_or_assign(Query(realm->read_group().get_table("class_object")));
1✔
1930
                std::move(mut_subs).commit();
1✔
1931
            }
1✔
1932

1933
            realm->sync_session()->force_close();
1✔
1934
            generate_event(realm, 0);
1✔
1935
            get_audit_events_from_baas(session, *session.app()->current_user(), 1);
1✔
1936
        }
1✔
1937
    }
3✔
1938

1939
    SECTION("creating audit event while offline uploads event when logged back in") {
14✔
1940
        auto sync_user = session.app()->current_user();
1✔
1941
        auto creds = create_user_and_log_in(session.app());
1✔
1942
        auto audit_user = session.app()->current_user();
1✔
1943
        config.audit_config->audit_user = audit_user;
1✔
1944
        config.audit_config->sync_error_handler = [&](SyncError error) {
1✔
NEW
1945
            REALM_ASSERT(ErrorCodes::error_categories(error.status.code()).test(ErrorCategory::app_error));
×
NEW
1946
        };
×
1947
        auto realm = Realm::get_shared_realm(config);
1✔
1948

1949
        audit_user->log_out();
1✔
1950
        generate_event(realm);
1✔
1951
        log_in_user(session.app(), creds);
1✔
1952

1953
        REQUIRE(get_audit_events_from_baas(session, *sync_user, 1).size() == 1);
1!
1954
    }
1✔
1955

1956
    SECTION("files with invalid client file idents are recovered") {
14✔
1957
        auto sync_user = session.app()->current_user();
1✔
1958
        auto creds = create_user_and_log_in(session.app());
1✔
1959
        auto audit_user = session.app()->current_user();
1✔
1960
        config.audit_config->audit_user = audit_user;
1✔
1961
        config.audit_config->sync_error_handler = [&](SyncError error) {
2✔
1962
            REALM_ASSERT(ErrorCodes::error_categories(error.status.code()).test(ErrorCategory::app_error));
2✔
1963
        };
2✔
1964
        auto realm = Realm::get_shared_realm(config);
1✔
1965
        audit_user->log_out();
1✔
1966

1967
        auto audit = realm->audit_context();
1✔
1968
        REQUIRE(audit);
1!
1969

1970
        // Set a small shard size so that we don't have to write an absurd
1971
        // amount of data to test this
1972
        audit_test_hooks::set_maximum_shard_size(32 * 1024);
1✔
1973
        auto cleanup = util::make_scope_exit([]() noexcept {
1✔
1974
            audit_test_hooks::set_maximum_shard_size(256 * 1024 * 1024);
1✔
1975
        });
1✔
1976

1977
        realm->begin_transaction();
1✔
1978
        auto table = realm->read_group().get_table("class_object");
1✔
1979
        std::vector<Obj> objects;
1✔
1980
        for (int i = 0; i < 2000; ++i)
2,001✔
1981
            objects.push_back(table->create_object_with_primary_key(i));
2,000✔
1982
        realm->commit_transaction();
1✔
1983

1984
        // Write a lot of audit scopes while unable to sync
1985
        for (int i = 0; i < 50; ++i) {
51✔
1986
            auto scope = audit->begin_scope(util::format("scope %1", i));
50✔
1987
            Results(realm, table->where()).snapshot();
50✔
1988
            audit->end_scope(scope, assert_no_error);
50✔
1989
        }
50✔
1990
        audit->wait_for_completion();
1✔
1991

1992
        // Client file idents aren't reread while a session is active, so we need
1993
        // to close all of the open audit Realms awaiting upload
1994
        realm->close();
1✔
1995
        realm = nullptr;
1✔
1996
        auto sync_manager = session.sync_manager();
1✔
1997
        for (auto& session : sync_manager->get_all_sessions()) {
1✔
1998
            session->shutdown_and_wait();
1✔
1999
        }
1✔
2000

2001
        // Set the client file ident for all pending Realms to an invalid one so
2002
        // that they'll get client resets
2003
        auto root = util::format("%1/realm-audit/%2/%3/audit", *session.config().storage_path,
1✔
2004
                                 session.app()->app_id(), audit_user->user_id());
1✔
2005
        std::string file_name;
1✔
2006
        util::DirScanner dir(root);
1✔
2007
        while (dir.next(file_name)) {
28✔
2008
            if (!StringData(file_name).ends_with(".realm") || StringData(file_name).contains(".backup."))
27✔
2009
                continue;
18✔
2010
            sync::ClientReplication repl;
9✔
2011
            auto db = DB::create(repl, root + "/" + file_name);
9✔
2012
            static_cast<sync::ClientHistory*>(repl._get_history_write())->set_client_file_ident({123, 456}, false);
9✔
2013
        }
9✔
2014

2015
        // Log the user back in and reopen the parent Realm to start trying to upload the audit data
2016
        log_in_user(session.app(), creds);
1✔
2017
        realm = Realm::get_shared_realm(config);
1✔
2018
        audit = realm->audit_context();
1✔
2019
        REQUIRE(audit);
1!
2020
        audit->wait_for_uploads();
1✔
2021

2022
        auto events = get_audit_events_from_baas(session, *sync_user, 50);
1✔
2023
        REQUIRE(events.size() == 50);
1!
2024
        for (int i = 0; i < 50; ++i) {
51✔
2025
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
2026
        }
50✔
2027
    }
1✔
2028

2029
#if 0 // This test takes ~10 minutes to run
2030
    SECTION("large audit scope") {
2031
        auto realm = Realm::get_shared_realm(config);
2032
        auto table = realm->read_group().get_table("class_object");
2033
        auto audit = realm->audit_context();
2034

2035
        realm->begin_transaction();
2036
        auto obj1 = table->create_object_with_primary_key(1);
2037
        auto obj2 = table->create_object_with_primary_key(2);
2038
        realm->commit_transaction();
2039

2040
        auto scope = audit->begin_scope("large");
2041
        for (int i = 0; i < 150'000; ++i) {
2042
            Object(realm, obj1);
2043
            Object(realm, obj2);
2044
        }
2045
        audit->end_scope(scope, assert_no_error);
2046

2047
        REQUIRE(get_audit_events_from_baas(session, *session.app()->current_user(), 300'000).size() == 300'000);
2048
    }
2049
#endif
2050
}
14✔
2051
#endif // REALM_ENABLE_AUTH_TESTS
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