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

realm / realm-core / thomas.goyne_849

26 Feb 2025 08:09PM UTC coverage: 91.115% (-0.004%) from 91.119%
thomas.goyne_849

Pull #8072

Evergreen

tgoyne
Enable automatic client reset handling for audit Realms
Pull Request #8072: Enable automatic client reset handling for audit Realms

102732 of 181546 branches covered (56.59%)

70 of 72 new or added lines in 2 files covered. (97.22%)

78 existing lines in 12 files now uncovered.

217417 of 238618 relevant lines covered (91.12%)

6107066.8 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

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

45
#include <realm/util/logger.hpp>
46

47
#include <catch2/catch_all.hpp>
48

49
#include <external/json/json.hpp>
50

51
using namespace realm;
52
using namespace std::string_literals;
53
using Catch::Matchers::StartsWith;
54
using nlohmann::json;
55

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

63
namespace {
64

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

74
util::Optional<std::string> to_optional_string(StringData sd)
75
{
393✔
76
    return sd ? util::Optional<std::string>(sd) : none;
393✔
77
}
393✔
78

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

100
    // Stop the sync server so that we can safely inspect its Realm files
101
    auto& server = manager.sync_server();
50✔
102
    server.stop();
50✔
103

104
    std::vector<AuditEvent> events;
50✔
105

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

117
        Group g(root + "/" + file_name);
64✔
118
        auto table = g.get_table("class_AuditEvent");
64✔
119
        if (!table)
64✔
120
            continue;
×
121

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

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

160
    return events;
50✔
161
}
50✔
162

163
void sort_events(std::vector<AuditEvent>& events)
164
{
10✔
165
    std::sort(events.begin(), events.end(), [](auto& a, auto& b) {
857✔
166
        return a.timestamp < b.timestamp;
857✔
167
    });
857✔
168
}
10✔
169

170
#if REALM_ENABLE_AUTH_TESTS
171
static std::vector<AuditEvent> get_audit_events_from_baas(TestAppSession& session, app::User& user,
172
                                                          size_t expected_count)
173
{
7✔
174
    static const std::set<std::string> nonmetadata_fields = {"activity", "event", "data", "realm_id"};
7✔
175

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

200
// Check that the given key is present and the value is null
201
#define REQUIRE_NULL(v, k)                                                                                           \
202
    do {                                                                                                             \
8✔
203
        REQUIRE(v.contains(k));                                                                                      \
8✔
204
        REQUIRE(v[k] == nullptr);                                                                                    \
8✔
205
    } while (0)
8✔
206

207
#define REQUIRE_SET_EQUAL(a, ...)                                                                                    \
208
    do {                                                                                                             \
19✔
209
        json actual = (a);                                                                                           \
19✔
210
        json expected = __VA_ARGS__;                                                                                 \
19✔
211
        std::sort(actual.begin(), actual.end());                                                                     \
19✔
212
        std::sort(expected.begin(), expected.end());                                                                 \
19✔
213
        REQUIRE(actual == expected);                                                                                 \
19✔
214
    } while (0)
19✔
215

216
class CustomSerializer : public AuditObjectSerializer {
217
public:
218
    const Obj* expected_obj = nullptr;
219
    bool error = false;
220
    size_t completion_count = 0;
221

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

238
    void scope_complete() override
239
    {
40✔
240
        ++completion_count;
40✔
241
    }
40✔
242
};
243

244
void assert_no_error(std::exception_ptr e)
245
{
484✔
246
    REALM_ASSERT(!e);
484✔
247
}
484✔
248

249
struct TestClock {
250
    std::atomic<int32_t> timestamp = 1000;
251
    TestClock()
252
    {
29✔
253
        audit_test_hooks::set_clock([&] {
331✔
254
            auto now = timestamp.fetch_add(1);
331✔
255
            return Timestamp(now, now);
331✔
256
        });
331✔
257
    }
29✔
258
    ~TestClock()
259
    {
29✔
260
        audit_test_hooks::set_clock(nullptr);
29✔
261
    }
29✔
262
};
263

264
} // namespace
265

266
TEST_CASE("audit object serialization", "[sync][pbs][audit]") {
36✔
267
    TestSyncManager test_session;
36✔
268
    SyncTestFile config(test_session, "parent");
36✔
269
    config.automatic_change_notifications = false;
36✔
270
    config.schema_version = 1;
36✔
271
    config.schema = Schema{
36✔
272
        {"object",
36✔
273
         {{"_id", PropertyType::Int, Property::IsPrimary{true}},
36✔
274

275
          {"int", PropertyType::Int | PropertyType::Nullable},
36✔
276
          {"bool", PropertyType::Bool | PropertyType::Nullable},
36✔
277
          {"string", PropertyType::String | PropertyType::Nullable},
36✔
278
          {"data", PropertyType::Data | PropertyType::Nullable},
36✔
279
          {"date", PropertyType::Date | PropertyType::Nullable},
36✔
280
          {"float", PropertyType::Float | PropertyType::Nullable},
36✔
281
          {"double", PropertyType::Double | PropertyType::Nullable},
36✔
282
          {"mixed", PropertyType::Mixed | PropertyType::Nullable},
36✔
283
          {"objectid", PropertyType::ObjectId | PropertyType::Nullable},
36✔
284
          {"decimal", PropertyType::Decimal | PropertyType::Nullable},
36✔
285
          {"uuid", PropertyType::UUID | PropertyType::Nullable},
36✔
286

287
          {"int list", PropertyType::Int | PropertyType::Nullable | PropertyType::Array},
36✔
288
          {"int set", PropertyType::Int | PropertyType::Nullable | PropertyType::Set},
36✔
289
          {"int dictionary", PropertyType::Int | PropertyType::Nullable | PropertyType::Dictionary},
36✔
290

291
          {"object", PropertyType::Object | PropertyType::Nullable, "target"},
36✔
292
          {"object list", PropertyType::Object | PropertyType::Array, "target"},
36✔
293
          {"object set", PropertyType::Object | PropertyType::Set, "target"},
36✔
294
          {"object dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary, "target"},
36✔
295

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

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

318
    auto table = realm->read_group().get_table("class_object");
36✔
319
    auto target_table = realm->read_group().get_table("class_target");
36✔
320
    CppContext context(realm);
36✔
321

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

334
        auto int_list = obj.get_list<util::Optional<int64_t>>("int list");
4✔
335
        int_list.add(1);
4✔
336
        int_list.add(2);
4✔
337
        int_list.add(3);
4✔
338
        int_list.add(none);
4✔
339

340
        auto int_set = obj.get_set<util::Optional<int64_t>>("int set");
4✔
341
        int_set.insert(1);
4✔
342
        int_set.insert(2);
4✔
343
        int_set.insert(3);
4✔
344
        int_set.insert(none);
4✔
345

346
        auto int_dictionary = obj.get_dictionary("int dictionary");
4✔
347
        int_dictionary.insert("1", 1);
4✔
348
        int_dictionary.insert("2", 2);
4✔
349
        int_dictionary.insert("3", 3);
4✔
350
        int_dictionary.insert("4", none);
4✔
351

352
        auto obj_list = obj.get_linklist("object list");
4✔
353
        obj_list.add(target_table->create_object_with_primary_key(1).set_all(1).get_key());
4✔
354
        obj_list.add(target_table->create_object_with_primary_key(2).set_all(2).get_key());
4✔
355
        obj_list.add(target_table->create_object_with_primary_key(3).set_all(3).get_key());
4✔
356

357
        auto obj_set = obj.get_linkset(obj.get_table()->get_column_key("object set"));
4✔
358
        obj_set.insert(target_table->create_object_with_primary_key(4).set_all(4).get_key());
4✔
359
        obj_set.insert(target_table->create_object_with_primary_key(5).set_all(5).get_key());
4✔
360
        obj_set.insert(target_table->create_object_with_primary_key(6).set_all(6).get_key());
4✔
361

362
        auto obj_dict = obj.get_dictionary("object dictionary");
4✔
363
        obj_dict.insert("a", target_table->create_object_with_primary_key(7).set_all(7).get_key());
4✔
364
        obj_dict.insert("b", target_table->create_object_with_primary_key(8).set_all(8).get_key());
4✔
365
        obj_dict.insert("c", target_table->create_object_with_primary_key(9).set_all(9).get_key());
4✔
366

367
        auto embedded_list = obj.get_linklist("embedded object list");
4✔
368
        embedded_list.create_and_insert_linked_object(0).set_all(1);
4✔
369
        embedded_list.create_and_insert_linked_object(1).set_all(2);
4✔
370
        embedded_list.create_and_insert_linked_object(2).set_all(3);
4✔
371

372
        auto embedded_dict = obj.get_dictionary("embedded object dictionary");
4✔
373
        embedded_dict.create_and_insert_linked_object("d").set_all(4);
4✔
374
        embedded_dict.create_and_insert_linked_object("e").set_all(5);
4✔
375
        embedded_dict.create_and_insert_linked_object("f").set_all(6);
4✔
376

377
        return obj;
4✔
378
    };
4✔
379

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

402
    SECTION("default object serialization") {
36✔
403
        realm->begin_transaction();
1✔
404
        auto obj = table->create_object_with_primary_key(2);
1✔
405
        populate_object(obj);
1✔
406
        realm->commit_transaction();
1✔
407

408
        auto scope = audit->begin_scope("scope");
1✔
409
        Object object(realm, obj);
1✔
410
        audit->end_scope(scope, assert_no_error);
1✔
411
        audit->wait_for_completion();
1✔
412

413
        auto events = get_audit_events(test_session);
1✔
414
        REQUIRE(events.size() == 1);
1!
415
        auto& event = events[0];
1✔
416
        REQUIRE(event.event == "read");
1!
417
        REQUIRE(event.activity == "scope");
1!
418
        REQUIRE(!event.timestamp.is_null());
1!
419

420
        REQUIRE(event.data["type"] == "object");
1!
421
        auto value = event.data["value"];
1✔
422
        REQUIRE(value.size() == 1);
1!
423
        validate_default_values(value[0]);
1✔
424
    }
1✔
425

426
    SECTION("custom object serialization") {
36✔
427
        realm->begin_transaction();
1✔
428
        auto obj1 = table->create_object_with_primary_key(2);
1✔
429
        auto obj2 = table->create_object_with_primary_key(3);
1✔
430
        realm->commit_transaction();
1✔
431

432
        serializer->expected_obj = &obj1;
1✔
433

434
        auto scope = audit->begin_scope("scope 1");
1✔
435
        Object(realm, obj1);
1✔
436
        audit->end_scope(scope, assert_no_error);
1✔
437
        audit->wait_for_completion();
1✔
438
        REQUIRE(serializer->completion_count == 1);
1!
439

440
        scope = audit->begin_scope("empty scope");
1✔
441
        audit->end_scope(scope, assert_no_error);
1✔
442
        audit->wait_for_completion();
1✔
443
        REQUIRE(serializer->completion_count == 2);
1!
444

445
        serializer->expected_obj = &obj2;
1✔
446

447
        scope = audit->begin_scope("scope 2");
1✔
448
        Object(realm, obj2);
1✔
449
        audit->end_scope(scope, assert_no_error);
1✔
450
        audit->wait_for_completion();
1✔
451
        REQUIRE(serializer->completion_count == 3);
1!
452

453
        auto events = get_audit_events(test_session);
1✔
454
        REQUIRE(events.size() == 2);
1!
455

456
        REQUIRE(events[0].activity == "scope 1");
1!
457
        REQUIRE(events[1].activity == "scope 2");
1!
458
        REQUIRE(events[0].data ==
1!
459
                json({{"type", "object"},
1✔
460
                      {"value", json::array({{{"obj", obj1.get_key().value}, {"table", table->get_key().value}}})}}));
1✔
461
        REQUIRE(events[1].data ==
1!
462
                json({{"type", "object"},
1✔
463
                      {"value", json::array({{{"obj", obj2.get_key().value}, {"table", table->get_key().value}}})}}));
1✔
464
    }
1✔
465

466
    SECTION("custom serialization error reporting") {
36✔
467
        serializer->error = true;
1✔
468

469
        realm->begin_transaction();
1✔
470
        auto obj = table->create_object_with_primary_key(2);
1✔
471
        realm->commit_transaction();
1✔
472
        auto scope = audit->begin_scope("scope");
1✔
473
        Object(realm, obj);
1✔
474
        audit->end_scope(scope, [](auto error) {
1✔
475
            REQUIRE(error);
1!
476
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "custom serialization error");
1✔
477
        });
1✔
478
        audit->wait_for_completion();
1✔
479
    }
1✔
480

481
    SECTION("write transaction serialization") {
36✔
482
        SECTION("create object") {
6✔
483
            auto scope = audit->begin_scope("scope");
1✔
484
            realm->begin_transaction();
1✔
485
            auto obj = table->create_object_with_primary_key(2);
1✔
486
            populate_object(obj);
1✔
487
            realm->commit_transaction();
1✔
488
            audit->end_scope(scope, assert_no_error);
1✔
489
            audit->wait_for_completion();
1✔
490

491
            auto events = get_audit_events(test_session);
1✔
492
            REQUIRE(events.size() == 1);
1!
493
            auto& event = events[0];
1✔
494
            REQUIRE(event.event == "write");
1!
495
            REQUIRE(event.activity == "scope");
1!
496
            REQUIRE(!event.timestamp.is_null());
1!
497

498
            REQUIRE(event.data.size() == 2);
1!
499
            auto& object_changes = event.data["object"];
1✔
500
            REQUIRE(object_changes.size() == 1);
1!
501
            REQUIRE(object_changes["insertions"].size() == 1);
1!
502
            validate_default_values(object_changes["insertions"][0]);
1✔
503

504
            // target table should have 9 insertions with _id == value
505
            REQUIRE(event.data["target"]["insertions"].size() == 9);
1!
506
            for (int i = 0; i < 9; ++i) {
10✔
507
                REQUIRE(event.data["target"]["insertions"][i] == json({{"_id", i + 1}, {"value", i + 1}}));
9!
508
            }
9✔
509
        }
1✔
510

511
        SECTION("modify object") {
6✔
512
            realm->begin_transaction();
1✔
513
            auto obj = table->create_object_with_primary_key(2);
1✔
514
            populate_object(obj);
1✔
515
            realm->commit_transaction();
1✔
516

517
            auto scope = audit->begin_scope("scope");
1✔
518
            realm->begin_transaction();
1✔
519
            obj.set("int", 3);
1✔
520
            obj.set("bool", true);
1✔
521
            realm->commit_transaction();
1✔
522
            audit->end_scope(scope, assert_no_error);
1✔
523
            audit->wait_for_completion();
1✔
524

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

539
        SECTION("delete object") {
6✔
540
            realm->begin_transaction();
1✔
541
            auto obj = table->create_object_with_primary_key(2);
1✔
542
            populate_object(obj);
1✔
543
            realm->commit_transaction();
1✔
544

545
            auto scope = audit->begin_scope("scope");
1✔
546
            realm->begin_transaction();
1✔
547
            obj.remove();
1✔
548
            realm->commit_transaction();
1✔
549
            audit->end_scope(scope, assert_no_error);
1✔
550
            audit->wait_for_completion();
1✔
551

552
            auto events = get_audit_events(test_session);
1✔
553
            REQUIRE(events.size() == 1);
1!
554
            auto& event = events[0];
1✔
555
            REQUIRE(event.data.size() == 1);
1!
556
            REQUIRE(event.data["object"].size() == 1);
1!
557
            REQUIRE(event.data["object"]["deletions"].size() == 1);
1!
558
            validate_default_values(event.data["object"]["deletions"][0]);
1✔
559
        }
1✔
560

561
        SECTION("delete embedded object") {
6✔
562
            realm->begin_transaction();
1✔
563
            auto obj = table->create_object_with_primary_key(2);
1✔
564
            obj.create_and_set_linked_object(obj.get_table()->get_column_key("embedded object")).set_all(100);
1✔
565
            realm->commit_transaction();
1✔
566

567
            auto scope = audit->begin_scope("scope");
1✔
568
            realm->begin_transaction();
1✔
569
            obj.get_linked_object("embedded object").remove();
1✔
570
            realm->commit_transaction();
1✔
571
            audit->end_scope(scope, assert_no_error);
1✔
572
            audit->wait_for_completion();
1✔
573

574
            auto events = get_audit_events(test_session);
1✔
575
            REQUIRE(events.size() == 1);
1!
576
            REQUIRE(events[0].data.size() == 1);
1!
577
            REQUIRE(events[0].data["object"].size() == 1);
1!
578
            REQUIRE(events[0].data["object"]["modifications"].size() == 1);
1!
579
            auto& modification = events[0].data["object"]["modifications"][0];
1✔
580
            REQUIRE(modification["newValue"] == json({{"embedded object", nullptr}}));
1!
581
            REQUIRE(modification["oldValue"]["embedded object"] == json({{"value", 100}}));
1!
582
        }
1✔
583

584
        SECTION("mixed changes") {
6✔
585
            realm->begin_transaction();
1✔
586
            std::vector<Obj> objects;
1✔
587
            for (int i = 0; i < 5; ++i)
6✔
588
                objects.push_back(target_table->create_object_with_primary_key(i).set_all(i));
5✔
589
            realm->commit_transaction();
1✔
590

591
            auto scope = audit->begin_scope("scope");
1✔
592
            realm->begin_transaction();
1✔
593

594
            // Mutate then delete should not report the mutate
595
            objects[0].set("value", 100);
1✔
596
            objects[1].set("value", 100);
1✔
597
            objects[2].set("value", 100);
1✔
598
            objects[1].remove();
1✔
599

600
            // Insert then mutate should not report the mutate
601
            auto obj = target_table->create_object_with_primary_key(20);
1✔
602
            obj.set("value", 100);
1✔
603

604
            // Insert then delete should not report the insert or delete
605
            auto obj2 = target_table->create_object_with_primary_key(21);
1✔
606
            obj2.remove();
1✔
607

608
            realm->commit_transaction();
1✔
609
            audit->end_scope(scope, assert_no_error);
1✔
610
            audit->wait_for_completion();
1✔
611

612
            auto events = get_audit_events(test_session);
1✔
613
            REQUIRE(events.size() == 1);
1!
614
            auto& event = events[0];
1✔
615
            REQUIRE(event.data.size() == 1);
1!
616
            auto& data = event.data["target"];
1✔
617
            REQUIRE(data.size() == 3);
1!
618
            REQUIRE(data["deletions"] == json({{{"_id", 1}, {"value", 1}}}));
1!
619
            REQUIRE(data["insertions"] == json({{{"_id", 20}, {"value", 100}}}));
1!
620
            REQUIRE(data["modifications"] ==
1!
621
                    json({{{"oldValue", {{"_id", 0}, {"value", 0}}}, {"newValue", {{"value", 100}}}},
1✔
622
                          {{"oldValue", {{"_id", 2}, {"value", 2}}}, {"newValue", {{"value", 100}}}}}));
1✔
623
        }
1✔
624

625
        SECTION("empty write transactions do not produce an event") {
6✔
626
            auto scope = audit->begin_scope("scope");
1✔
627
            realm->begin_transaction();
1✔
628
            realm->commit_transaction();
1✔
629
            audit->end_scope(scope, assert_no_error);
1✔
630
            audit->wait_for_completion();
1✔
631

632
            REQUIRE(get_audit_events(test_session).empty());
1!
633
        }
1✔
634
    }
6✔
635

636
    SECTION("empty query") {
36✔
637
        auto scope = audit->begin_scope("scope");
1✔
638
        Results(realm, table->where()).snapshot();
1✔
639
        audit->end_scope(scope, assert_no_error);
1✔
640
        audit->wait_for_completion();
1✔
641
        REQUIRE(get_audit_events(test_session).empty());
1!
642
    }
1✔
643

644
    SECTION("non-empty query") {
36✔
645
        realm->begin_transaction();
5✔
646
        for (int64_t i = 0; i < 10; ++i) {
55✔
647
            table->create_object_with_primary_key(i);
50✔
648
            target_table->create_object_with_primary_key(i);
50✔
649
        }
50✔
650
        realm->commit_transaction();
5✔
651

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

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

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

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

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

715
    SECTION("query on list of objects") {
36✔
716
        realm->begin_transaction();
1✔
717
        auto obj = table->create_object_with_primary_key(2);
1✔
718
        auto list = obj.get_linklist("object list");
1✔
719
        for (int64_t i = 0; i < 10; ++i)
11✔
720
            list.add(target_table->create_object_with_primary_key(i).set_all(i * 2).get_key());
10✔
721
        realm->commit_transaction();
1✔
722

723
        auto scope = audit->begin_scope("scope");
1✔
724
        Object object(realm, obj);
1✔
725
        auto obj_list = util::any_cast<List>(object.get_property_value<std::any>(context, "object list"));
1✔
726
        obj_list.filter(target_table->where().greater(target_table->get_column_key("value"), 10)).snapshot();
1✔
727
        audit->end_scope(scope, assert_no_error);
1✔
728
        audit->wait_for_completion();
1✔
729

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

743
    SECTION("link access tracking") {
36✔
744
        realm->begin_transaction();
16✔
745
        table->create_object_with_primary_key(1);
16✔
746
        target_table->create_object_with_primary_key(0);
16✔
747
        auto obj = table->create_object_with_primary_key(2);
16✔
748
        obj.set("object", target_table->create_object_with_primary_key(1).set_all(1).get_key());
16✔
749
        obj.create_and_set_linked_object(table->get_column_key("embedded object")).set_all(200);
16✔
750

751
        auto obj_list = obj.get_linklist("object list");
16✔
752
        obj_list.add(target_table->create_object_with_primary_key(3).set_all(10).get_key());
16✔
753
        obj_list.add(target_table->create_object_with_primary_key(4).set_all(20).get_key());
16✔
754
        obj_list.add(target_table->create_object_with_primary_key(5).set_all(30).get_key());
16✔
755

756
        auto obj_set = obj.get_linkset(obj.get_table()->get_column_key("object set"));
16✔
757
        obj_set.insert(target_table->create_object_with_primary_key(6).set_all(40).get_key());
16✔
758
        obj_set.insert(target_table->create_object_with_primary_key(7).set_all(50).get_key());
16✔
759
        obj_set.insert(target_table->create_object_with_primary_key(8).set_all(60).get_key());
16✔
760

761
        auto obj_dict = obj.get_dictionary("object dictionary");
16✔
762
        obj_dict.insert("a", target_table->create_object_with_primary_key(9).set_all(90).get_key());
16✔
763
        obj_dict.insert("b", target_table->create_object_with_primary_key(10).set_all(100).get_key());
16✔
764
        obj_dict.insert("c", target_table->create_object_with_primary_key(11).set_all(110).get_key());
16✔
765
        realm->commit_transaction();
16✔
766

767
        SECTION("objects are serialized as just primary key by default") {
16✔
768
            auto scope = audit->begin_scope("scope");
1✔
769
            Object object(realm, obj);
1✔
770
            audit->end_scope(scope, assert_no_error);
1✔
771
            audit->wait_for_completion();
1✔
772

773
            auto events = get_audit_events(test_session);
1✔
774
            REQUIRE(events.size() == 1);
1!
775
            auto& value = events[0].data["value"][0];
1✔
776
            REQUIRE(value["object"] == 1);
1!
777
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
778
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
779
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
780
        }
1✔
781

782
        SECTION("embedded objects are always full object") {
16✔
783
            auto scope = audit->begin_scope("scope");
1✔
784
            Object object(realm, obj);
1✔
785
            audit->end_scope(scope, assert_no_error);
1✔
786
            audit->wait_for_completion();
1✔
787

788
            auto events = get_audit_events(test_session);
1✔
789
            REQUIRE(events.size() == 1);
1!
790
            REQUIRE(events[0].data["value"][0]["embedded object"] == json({{"value", 200}}));
1!
791
        }
1✔
792

793
        SECTION("links followed serialize the full object") {
16✔
794
            auto scope = audit->begin_scope("scope");
1✔
795
            Object object(realm, obj);
1✔
796
            object.get_property_value<std::any>(context, "object");
1✔
797
            audit->end_scope(scope, assert_no_error);
1✔
798
            audit->wait_for_completion();
1✔
799

800
            auto events = get_audit_events(test_session);
1✔
801
            REQUIRE(events.size() == 2);
1!
802
            auto& value = events[0].data["value"][0];
1✔
803
            REQUIRE(value["object"] == json({{"_id", 1}, {"value", 1}}));
1!
804
            REQUIRE(events[1].data["value"][0] == json({{"_id", 1}, {"value", 1}}));
1!
805

806
            // Other fields are left in pk form
807
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
808
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
809
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
810
        }
1✔
811

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

822
            auto events = get_audit_events(test_session);
1✔
823
            REQUIRE(events.size() == 1);
1!
824
            auto& value = events[0].data["value"][0];
1✔
825
            REQUIRE(value["object list"] == json({3, 4, 5}));
1!
826
            REQUIRE_SET_EQUAL(value["object set"], {6, 7, 8});
1!
827
            REQUIRE(value["object dictionary"] == json({{"a", 9}, {"b", 10}, {"c", 11}}));
1!
828
        }
1✔
829

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

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

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

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

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

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

909
        SECTION(
16✔
910
            "link access on an object read outside of a scope does not produce a read on the parent in the scope") {
1✔
911
            Object object(realm, obj);
1✔
912
            auto scope = audit->begin_scope("scope");
1✔
913
            object.get_property_value<std::any>(context, "object");
1✔
914
            audit->end_scope(scope, assert_no_error);
1✔
915
            audit->wait_for_completion();
1✔
916

917
            auto events = get_audit_events(test_session);
1✔
918
            REQUIRE(events.size() == 1);
1!
919
            auto& event = events[0];
1✔
920
            REQUIRE(event.event == "read");
1!
921
            REQUIRE(event.data["type"] == "target");
1!
922
        }
1✔
923

924
        SECTION("link access in a different scope from the object do not expand linked object in parent read") {
16✔
925
            auto scope = audit->begin_scope("scope 1");
1✔
926
            Object object(realm, obj);
1✔
927
            audit->end_scope(scope, assert_no_error);
1✔
928

929
            scope = audit->begin_scope("scope 2");
1✔
930
            object.get_property_value<std::any>(context, "object");
1✔
931
            audit->end_scope(scope, assert_no_error);
1✔
932
            audit->wait_for_completion();
1✔
933

934
            auto events = get_audit_events(test_session);
1✔
935
            REQUIRE(events.size() == 2);
1!
936
            REQUIRE(events[0].activity == "scope 1");
1!
937
            REQUIRE(events[0].data["type"] == "object");
1!
938
            REQUIRE(events[1].activity == "scope 2");
1!
939
            REQUIRE(events[1].data["type"] == "target");
1!
940
            REQUIRE(events[0].data["value"][0]["object"] == 1);
1!
941
        }
1✔
942

943
        SECTION("link access tracking is reset between scopes") {
16✔
944
            auto scope = audit->begin_scope("scope 1");
1✔
945
            Object object(realm, obj);
1✔
946
            object.get_property_value<std::any>(context, "object");
1✔
947
            audit->end_scope(scope, assert_no_error);
1✔
948

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

958
            auto events = get_audit_events(test_session);
1✔
959
            REQUIRE(events.size() == 5);
1!
960
            REQUIRE(events[0].activity == "scope 1");
1!
961
            REQUIRE(events[1].activity == "scope 1");
1!
962
            REQUIRE(events[2].activity == "scope 2");
1!
963
            REQUIRE(events[3].activity == "scope 2");
1!
964
            REQUIRE(events[4].activity == "scope 2");
1!
965

966
            REQUIRE(events[0].data["type"] == "object");
1!
967
            REQUIRE(events[1].data["type"] == "target");
1!
968
            REQUIRE(events[2].data["type"] == "target");
1!
969
            REQUIRE(events[3].data["type"] == "target");
1!
970
            REQUIRE(events[4].data["type"] == "object");
1!
971

972
            // First link should be expanded, second, should not
973
            REQUIRE(events[0].data["value"][0]["object"] == json({{"_id", 1}, {"value", 1}}));
1!
974
            REQUIRE(events[4].data["value"][0]["object"] == 1);
1!
975
        }
1✔
976

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

980
            auto scope = audit->begin_scope("scope");
1✔
981
            object.get_property_value<std::any>(context, "object");
1✔
982
            Object(realm, obj);
1✔
983
            audit->end_scope(scope, assert_no_error);
1✔
984
            audit->wait_for_completion();
1✔
985

986
            auto events = get_audit_events(test_session);
1✔
987
            REQUIRE(events.size() == 2);
1!
988
            REQUIRE(events[1].data["value"][0]["object"] == 1);
1!
989
        }
1✔
990
    }
16✔
991

992
    SECTION("read on newly created object") {
36✔
993
        realm->begin_transaction();
1✔
994
        auto scope = audit->begin_scope("scope");
1✔
995
        Object object(realm, table->create_object_with_primary_key(100));
1✔
996
        Results(realm, table->where()).snapshot();
1✔
997
        audit->end_scope(scope, assert_no_error);
1✔
998
        realm->commit_transaction();
1✔
999
        audit->wait_for_completion();
1✔
1000

1001
        auto events = get_audit_events(test_session);
1✔
1002
        REQUIRE(events.empty());
1!
1003
    }
1✔
1004

1005
    SECTION("query matching both new and existing objects") {
36✔
1006
        realm->begin_transaction();
1✔
1007
        table->create_object_with_primary_key(1);
1✔
1008
        realm->commit_transaction();
1✔
1009

1010
        realm->begin_transaction();
1✔
1011
        table->create_object_with_primary_key(2);
1✔
1012
        auto scope = audit->begin_scope("scope");
1✔
1013
        Results(realm, table->where()).snapshot();
1✔
1014
        audit->end_scope(scope, assert_no_error);
1✔
1015
        realm->commit_transaction();
1✔
1016
        audit->wait_for_completion();
1✔
1017

1018
        auto events = get_audit_events(test_session);
1✔
1019
        REQUIRE(events.size() == 1);
1!
1020
        REQUIRE(events[0].data["value"].size() == 1);
1!
1021
    }
1✔
1022

1023
    SECTION("reads mixed with deletions") {
36✔
1024
        realm->begin_transaction();
2✔
1025
        table->create_object_with_primary_key(1);
2✔
1026
        auto obj2 = table->create_object_with_primary_key(2);
2✔
1027
        auto obj3 = table->create_object_with_primary_key(3);
2✔
1028
        realm->commit_transaction();
2✔
1029

1030
        SECTION("reads of objects that are subsequently deleted are still reported") {
2✔
1031
            auto scope = audit->begin_scope("scope");
1✔
1032
            realm->begin_transaction();
1✔
1033
            Object(realm, obj2);
1✔
1034
            obj2.remove();
1✔
1035
            realm->commit_transaction();
1✔
1036
            audit->end_scope(scope, assert_no_error);
1✔
1037
            audit->wait_for_completion();
1✔
1038

1039
            auto events = get_audit_events(test_session);
1✔
1040
            REQUIRE(events.size() == 2);
1!
1041
            REQUIRE(events[0].event == "read");
1!
1042
            REQUIRE(events[1].event == "write");
1!
1043
            REQUIRE(events[0].data["value"][0]["_id"] == 2);
1!
1044
        }
1✔
1045

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

1057
            auto events = get_audit_events(test_session);
1✔
1058
            REQUIRE(events.size() == 2);
1!
1059
            REQUIRE(events[0].event == "read");
1!
1060
            REQUIRE(events[1].event == "write");
1!
1061
            REQUIRE(events[0].data["value"][0]["_id"] == 3);
1!
1062
        }
1✔
1063
    }
2✔
1064
}
36✔
1065

1066
TEST_CASE("audit management", "[sync][pbs][audit]") {
15✔
1067
    TestClock clock;
15✔
1068

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

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

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

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

1122
    SECTION("scope names") {
15✔
1123
        realm->begin_transaction();
1✔
1124
        auto obj = table->create_object_with_primary_key(1);
1✔
1125
        realm->commit_transaction();
1✔
1126

1127
        auto scope = audit->begin_scope("scope 1");
1✔
1128
        Object(realm, obj);
1✔
1129
        audit->end_scope(scope, assert_no_error);
1✔
1130

1131
        scope = audit->begin_scope("scope 2");
1✔
1132
        Object(realm, obj);
1✔
1133
        audit->end_scope(scope, assert_no_error);
1✔
1134
        audit->wait_for_completion();
1✔
1135

1136
        auto events = get_audit_events(test_session);
1✔
1137
        REQUIRE(events.size() == 2);
1!
1138
        REQUIRE(events[0].activity == "scope 1");
1!
1139
        REQUIRE(events[1].activity == "scope 2");
1!
1140
    }
1✔
1141

1142
    SECTION("nested scopes") {
15✔
1143
        realm->begin_transaction();
1✔
1144
        auto obj1 = table->create_object_with_primary_key(1);
1✔
1145
        auto obj2 = table->create_object_with_primary_key(2);
1✔
1146
        auto obj3 = table->create_object_with_primary_key(3);
1✔
1147
        realm->commit_transaction();
1✔
1148

1149
        auto scope1 = audit->begin_scope("scope 1");
1✔
1150
        Object(realm, obj1); // read in scope 1 only
1✔
1151

1152
        auto scope2 = audit->begin_scope("scope 2");
1✔
1153
        Object(realm, obj2); // read in both scopes
1✔
1154
        audit->end_scope(scope2, assert_no_error);
1✔
1155

1156
        Object(realm, obj3); // read in scope 1 only
1✔
1157

1158
        audit->end_scope(scope1, assert_no_error);
1✔
1159
        audit->wait_for_completion();
1✔
1160

1161
        auto events = get_audit_events(test_session);
1✔
1162
        REQUIRE(events.size() == 4);
1!
1163

1164
        // scope 2 read on obj 2 comes first as it was the first scope ended
1165
        REQUIRE(events[0].activity == "scope 2");
1!
1166
        REQUIRE(events[0].data["value"][0]["_id"] == 2);
1!
1167

1168
        // scope 1 then has reads on each object in order
1169
        REQUIRE(events[1].activity == "scope 1");
1!
1170
        REQUIRE(events[1].data["value"][0]["_id"] == 1);
1!
1171
        REQUIRE(events[2].activity == "scope 1");
1!
1172
        REQUIRE(events[2].data["value"][0]["_id"] == 2);
1!
1173
        REQUIRE(events[3].activity == "scope 1");
1!
1174
        REQUIRE(events[3].data["value"][0]["_id"] == 3);
1!
1175
    }
1✔
1176

1177
    SECTION("overlapping scopes") {
15✔
1178
        realm->begin_transaction();
1✔
1179
        auto obj1 = table->create_object_with_primary_key(1);
1✔
1180
        auto obj2 = table->create_object_with_primary_key(2);
1✔
1181
        auto obj3 = table->create_object_with_primary_key(3);
1✔
1182
        realm->commit_transaction();
1✔
1183

1184
        auto scope1 = audit->begin_scope("scope 1");
1✔
1185
        Object(realm, obj1); // read in scope 1 only
1✔
1186

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

1190
        audit->end_scope(scope1, assert_no_error);
1✔
1191
        Object(realm, obj3); // read in scope 2 only
1✔
1192

1193
        audit->end_scope(scope2, assert_no_error);
1✔
1194
        audit->wait_for_completion();
1✔
1195

1196
        auto events = get_audit_events(test_session);
1✔
1197
        REQUIRE(events.size() == 4);
1!
1198

1199
        // scope 1 only read on obj 1
1200
        REQUIRE(events[0].activity == "scope 1");
1!
1201
        REQUIRE(events[0].data["value"][0]["_id"] == 1);
1!
1202

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

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

1214
    SECTION("scope cancellation") {
15✔
1215
        realm->begin_transaction();
1✔
1216
        auto obj = table->create_object_with_primary_key(1);
1✔
1217
        realm->commit_transaction();
1✔
1218

1219
        auto scope1 = audit->begin_scope("scope 1");
1✔
1220
        auto scope2 = audit->begin_scope("scope 2");
1✔
1221
        Object(realm, obj);
1✔
1222
        audit->cancel_scope(scope1);
1✔
1223
        audit->end_scope(scope2, assert_no_error);
1✔
1224
        audit->wait_for_completion();
1✔
1225

1226
        auto events = get_audit_events(test_session);
1✔
1227
        REQUIRE(events.size() == 1);
1!
1228
        REQUIRE(events[0].activity == "scope 2");
1!
1229
    }
1✔
1230

1231
    SECTION("ending invalid scopes") {
15✔
1232
        REQUIRE_FALSE(audit->is_scope_valid(0));
1!
1233
        REQUIRE_THROWS_WITH(audit->end_scope(0),
1✔
1234
                            "Cannot end event scope: scope '0' not in progress. Scope may have already been ended?");
1✔
1235

1236
        auto scope = audit->begin_scope("scope");
1✔
1237
        REQUIRE(audit->is_scope_valid(scope));
1!
1238
        REQUIRE_NOTHROW(audit->end_scope(scope));
1✔
1239

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

1244
        scope = audit->begin_scope("scope 2");
1✔
1245
        REQUIRE(audit->is_scope_valid(scope));
1!
1246
        REQUIRE_NOTHROW(audit->cancel_scope(scope));
1✔
1247

1248
        REQUIRE_FALSE(audit->is_scope_valid(scope));
1!
1249
        REQUIRE_THROWS_WITH(audit->cancel_scope(scope),
1✔
1250
                            "Cannot end event scope: scope '2' not in progress. Scope may have already been ended?");
1✔
1251
    }
1✔
1252

1253
    SECTION("event timestamps") {
15✔
1254
        std::vector<Obj> objects;
1✔
1255
        realm->begin_transaction();
1✔
1256
        for (int i = 0; i < 10; ++i)
11✔
1257
            objects.push_back(table->create_object_with_primary_key(i));
10✔
1258
        realm->commit_transaction();
1✔
1259

1260
        auto scope = audit->begin_scope("scope");
1✔
1261
        for (int i = 0; i < 10; ++i) {
11✔
1262
            Object(realm, objects[i]);
10✔
1263
            Object(realm, objects[i]);
10✔
1264
        }
10✔
1265
        audit->end_scope(scope, assert_no_error);
1✔
1266
        audit->wait_for_completion();
1✔
1267

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

1277
    SECTION("metadata updating") {
15✔
1278
        realm->begin_transaction();
5✔
1279
        auto obj1 = realm->read_group().get_table("class_object")->create_object_with_primary_key(1);
5✔
1280
        realm->read_group().get_table("class_object")->create_object_with_primary_key(2);
5✔
1281
        realm->read_group().get_table("class_object")->create_object_with_primary_key(3);
5✔
1282
        realm->commit_transaction();
5✔
1283

1284
        SECTION("update before scope") {
5✔
1285
            audit->update_metadata({{"a", "aa"}});
1✔
1286
            auto scope = audit->begin_scope("scope 1");
1✔
1287
            Object(realm, obj1);
1✔
1288
            audit->end_scope(scope, assert_no_error);
1✔
1289
            audit->wait_for_completion();
1✔
1290

1291
            auto events = get_audit_events(test_session);
1✔
1292
            REQUIRE(events.size() == 1);
1!
1293
            auto event = events[0];
1✔
1294
            REQUIRE(event.metadata.size() == 1);
1!
1295
            REQUIRE(event.metadata["a"] == "aa");
1!
1296
        }
1✔
1297

1298
        SECTION("update during scope") {
5✔
1299
            auto scope = audit->begin_scope("scope 1");
1✔
1300
            audit->update_metadata({{"a", "aa"}});
1✔
1301
            Object(realm, obj1);
1✔
1302
            audit->end_scope(scope, assert_no_error);
1✔
1303
            audit->wait_for_completion();
1✔
1304

1305
            auto events = get_audit_events(test_session);
1✔
1306
            REQUIRE(events.size() == 1);
1!
1307
            auto event = events[0];
1✔
1308
            REQUIRE(event.metadata.size() == 0);
1!
1309
        }
1✔
1310

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

1320
            auto events = get_audit_events(test_session);
1✔
1321
            REQUIRE(events.size() == 100);
1!
1322
            for (size_t i = 0; i < 100; ++i) {
101✔
1323
                REQUIRE(events[i].metadata.size() == 1);
100!
1324
                REQUIRE(events[i].metadata[util::format("name %1", i)] == util::format("value %1", i));
100!
1325
            }
100✔
1326
        }
1✔
1327

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

1339
            auto events = get_audit_events(test_session);
1✔
1340
            REQUIRE(events.size() == 100);
1!
1341
            for (size_t i = 0; i < 100; ++i) {
101✔
1342
                REQUIRE(events[i].metadata.size() == i + 1);
100!
1343
            }
100✔
1344
        }
1✔
1345

1346
        SECTION("update via opening new realm") {
5✔
1347
            config.audit_config->metadata = {{"a", "aa"}};
1✔
1348
            auto realm2 = Realm::get_shared_realm(config);
1✔
1349
            auto obj2 = realm2->read_group().get_table("class_object")->get_object(1);
1✔
1350

1351
            auto scope = audit->begin_scope("scope 1");
1✔
1352
            Object(realm, obj1);
1✔
1353
            Object(realm2, obj2);
1✔
1354
            audit->end_scope(scope, assert_no_error);
1✔
1355

1356
            config.audit_config->metadata = {{"a", "aaa"}, {"b", "bb"}};
1✔
1357
            auto realm3 = Realm::get_shared_realm(config);
1✔
1358
            auto obj3 = realm3->read_group().get_table("class_object")->get_object(2);
1✔
1359

1360
            scope = audit->begin_scope("scope 2");
1✔
1361
            Object(realm, obj1);
1✔
1362
            Object(realm2, obj2);
1✔
1363
            Object(realm3, obj3);
1✔
1364
            audit->end_scope(scope, assert_no_error);
1✔
1365
            audit->wait_for_completion();
1✔
1366

1367
            auto events = get_audit_events(test_session);
1✔
1368
            REQUIRE(events.size() == 5);
1!
1369
            REQUIRE(events[0].activity == "scope 1");
1!
1370
            REQUIRE(events[1].activity == "scope 1");
1!
1371
            REQUIRE(events[2].activity == "scope 2");
1!
1372
            REQUIRE(events[3].activity == "scope 2");
1!
1373
            REQUIRE(events[4].activity == "scope 2");
1!
1374
            REQUIRE(events[0].metadata.size() == 1);
1!
1375
            REQUIRE(events[1].metadata.size() == 1);
1!
1376
            REQUIRE(events[2].metadata.size() == 2);
1!
1377
            REQUIRE(events[3].metadata.size() == 2);
1!
1378
            REQUIRE(events[4].metadata.size() == 2);
1!
1379
        }
1✔
1380
    }
5✔
1381

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

1393
        audit->record_event("event 1", "event"s, "data"s, expect_completion(0));
1✔
1394
        audit->record_event("event 2", none, "data"s, expect_completion(1));
1✔
1395
        auto scope = audit->begin_scope("scope");
1✔
1396
        // note: does not use the scope's activity
1397
        audit->record_event("event 3", none, none, expect_completion(2));
1✔
1398
        audit->end_scope(scope, expect_completion(3));
1✔
1399
        audit->record_event("event 4", none, none, expect_completion(4));
1✔
1400

1401
        util::EventLoop::main().run_until([&] {
182✔
1402
            return completions == 5;
182✔
1403
        });
182✔
1404

1405
        for (size_t i = 0; i < 5; ++i) {
6✔
1406
            REQUIRE(i == completion_results[i].first);
5!
1407
            REQUIRE_FALSE(completion_results[i].second);
5!
1408
        }
5✔
1409

1410
        auto events = get_audit_events(test_session, false);
1✔
1411
        REQUIRE(events.size() == 4);
1!
1412
        REQUIRE(events[0].activity == "event 1");
1!
1413
        REQUIRE(events[1].activity == "event 2");
1!
1414
        REQUIRE(events[2].activity == "event 3");
1!
1415
        REQUIRE(events[3].activity == "event 4");
1!
1416
        REQUIRE(events[0].event == "event"s);
1!
1417
        REQUIRE(events[1].event == none);
1!
1418
        REQUIRE(events[2].event == none);
1!
1419
        REQUIRE(events[3].event == none);
1!
1420
        REQUIRE(events[0].raw_data == "data"s);
1!
1421
        REQUIRE(events[1].raw_data == "data"s);
1!
1422
        REQUIRE(events[2].raw_data == none);
1!
1423
        REQUIRE(events[3].raw_data == none);
1!
1424
    }
1✔
1425

1426
    SECTION("read transaction version management") {
15✔
1427
        realm->begin_transaction();
1✔
1428
        auto obj = table->create_object_with_primary_key(1);
1✔
1429
        realm->commit_transaction();
1✔
1430

1431
        auto realm2 = Realm::get_shared_realm(config);
1✔
1432
        auto obj2 = realm2->read_group().get_table("class_object")->get_object(0);
1✔
1433
        auto realm3 = Realm::get_shared_realm(config);
1✔
1434
        auto obj3 = realm3->read_group().get_table("class_object")->get_object(0);
1✔
1435

1436
        realm2->begin_transaction();
1✔
1437
        obj2.set_all(1);
1✔
1438
        realm2->commit_transaction();
1✔
1439

1440
        realm3->begin_transaction();
1✔
1441
        obj3.set_all(2);
1✔
1442
        realm3->commit_transaction();
1✔
1443

1444
        auto scope = audit->begin_scope("scope");
1✔
1445
        Object(realm3, obj3); // value 2
1✔
1446
        Object(realm2, obj2); // value 1
1✔
1447
        Object(realm, obj);   // value 0
1✔
1448
        realm->refresh();
1✔
1449
        Object(realm, obj);   // value 2
1✔
1450
        Object(realm2, obj2); // value 1
1✔
1451
        realm2->refresh();
1✔
1452
        Object(realm3, obj3); // value 2
1✔
1453
        Object(realm2, obj2); // value 2
1✔
1454
        Object(realm, obj);   // value 2
1✔
1455
        audit->end_scope(scope, assert_no_error);
1✔
1456
        audit->wait_for_completion();
1✔
1457

1458
        auto events = get_audit_events(test_session);
1✔
1459
        REQUIRE(events.size() == 6);
1!
1460
        std::string str = events[0].data.dump();
1✔
1461
        // initial
1462
        REQUIRE(events[0].data["value"][0]["value"] == 2);
1!
1463
        REQUIRE(events[1].data["value"][0]["value"] == 1);
1!
1464
        REQUIRE(events[2].data["value"][0]["value"] == 0);
1!
1465

1466
        // realm->refresh()
1467
        REQUIRE(events[3].data["value"][0]["value"] == 2);
1!
1468
        REQUIRE(events[4].data["value"][0]["value"] == 1);
1!
1469

1470
        // realm2->refresh()
1471
        REQUIRE(events[5].data["value"][0]["value"] == 2);
1!
1472
    }
1✔
1473

1474
#if !REALM_DEBUG // This test is unreasonably slow in debug mode
1475
    SECTION("large audit scope") {
1476
        realm->begin_transaction();
1477
        auto obj1 = table->create_object_with_primary_key(1);
1478
        auto obj2 = table->create_object_with_primary_key(2);
1479
        realm->commit_transaction();
1480

1481
        auto scope = audit->begin_scope("large");
1482
        for (int i = 0; i < 150'000; ++i) {
1483
            Object(realm, obj1);
1484
            Object(realm, obj2);
1485
        }
1486
        audit->end_scope(scope, assert_no_error);
1487
        audit->wait_for_completion();
1488

1489
        auto events = get_audit_events(test_session);
1490
        REQUIRE(events.size() == 300'000);
1491
    }
1492
#endif
1493
}
15✔
1494

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

1500
    SyncTestFile config(test_session, "parent");
3✔
1501
    config.automatic_change_notifications = false;
3✔
1502
    config.schema_version = 1;
3✔
1503
    config.schema = Schema{
3✔
1504
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
3✔
1505
    };
3✔
1506
    config.audit_config = std::make_shared<AuditConfig>();
3✔
1507
    config.audit_config->base_file_path = test_session.base_file_path();
3✔
1508
    config.audit_config->logger = audit_logger;
3✔
1509
    auto realm = Realm::get_shared_realm(config);
3✔
1510
    auto audit = realm->audit_context();
3✔
1511
    REQUIRE(audit);
3!
1512

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

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

1519
    // Set a small shard size so that we don't have to write an absurd
1520
    // amount of data to test this
1521
    audit_test_hooks::set_maximum_shard_size(32 * 1024);
3✔
1522
    auto cleanup = util::make_scope_exit([]() noexcept {
3✔
1523
        audit_test_hooks::set_maximum_shard_size(256 * 1024 * 1024);
3✔
1524
    });
3✔
1525

1526
    realm->begin_transaction();
3✔
1527
    std::vector<Obj> objects;
3✔
1528
    for (int i = 0; i < 2000; ++i)
6,003✔
1529
        objects.push_back(table->create_object_with_primary_key(i));
6,000✔
1530
    realm->commit_transaction();
3✔
1531

1532
    // Write a lot of audit scopes while unable to sync
1533
    for (int i = 0; i < 50; ++i) {
153✔
1534
        auto scope = audit->begin_scope(util::format("scope %1", i));
150✔
1535
        Results(realm, table->where()).snapshot();
150✔
1536
        audit->end_scope(scope, assert_no_error);
150✔
1537
    }
150✔
1538
    audit->wait_for_completion();
3✔
1539

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

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

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

1593
    SECTION("start server with existing session open") {
3✔
1594
        test_session.sync_server().start();
1✔
1595
        audit->wait_for_uploads();
1✔
1596

1597
        auto events = get_sorted_events();
1✔
1598
        REQUIRE(events.size() == 50);
1!
1599
        for (int i = 0; i < 50; ++i) {
51✔
1600
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
1601
        }
50✔
1602

1603
        // There should be exactly one remaining local Realm file (the currently
1604
        // open one that hasn't hit the size limit yet)
1605
        size_t remaining_realms = 0;
1✔
1606
        util::DirScanner dir(root);
1✔
1607
        while (dir.next(file_name)) {
27✔
1608
            if (StringData(file_name).ends_with(".realm"))
26✔
1609
                ++remaining_realms;
1✔
1610
        }
26✔
1611
        REQUIRE(remaining_realms == 1);
1!
1612
    }
1✔
1613

1614
    SECTION("trigger uploading by opening a new Realm") {
3✔
1615
        close_all_sessions();
1✔
1616
        test_session.sync_server().start();
1✔
1617

1618
        // Open a different Realm with the same user and audit prefix
1619
        SyncTestFile config(test_session, "other");
1✔
1620
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1621
        config.audit_config->logger = audit_logger;
1✔
1622
        config.audit_config->base_file_path = test_session.base_file_path();
1✔
1623
        auto realm = Realm::get_shared_realm(config);
1✔
1624
        auto audit2 = realm->audit_context();
1✔
1625
        REQUIRE(audit2);
1!
1626
        audit2->wait_for_uploads();
1✔
1627

1628
        auto events = get_sorted_events();
1✔
1629
        REQUIRE(events.size() == 50);
1!
1630
        for (int i = 0; i < 50; ++i) {
51✔
1631
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
1632
        }
50✔
1633

1634
        // There should be no remaining local Realm files because we haven't
1635
        // made the new audit context open a Realm yet
1636
        util::DirScanner dir(root);
1✔
1637
        while (dir.next(file_name)) {
26✔
1638
            REQUIRE_FALSE(StringData(file_name).ends_with(".realm"));
25!
1639
        }
25✔
1640
    }
1✔
1641

1642
    SECTION("uploading is per audit prefix") {
3✔
1643
        close_all_sessions();
1✔
1644
        test_session.sync_server().start();
1✔
1645

1646
        // Open the same Realm with a different audit prefix
1647
        SyncTestFile config(test_session, "parent");
1✔
1648
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1649
        config.audit_config->base_file_path = test_session.base_file_path();
1✔
1650
        config.audit_config->logger = audit_logger;
1✔
1651
        config.audit_config->partition_value_prefix = "other";
1✔
1652
        auto realm = Realm::get_shared_realm(config);
1✔
1653
        auto audit2 = realm->audit_context();
1✔
1654
        REQUIRE(audit2);
1!
1655
        audit2->wait_for_uploads();
1✔
1656

1657
        // Should not have uploaded any of the old events
1658
        auto events = get_sorted_events();
1✔
1659
        REQUIRE(events.size() == 0);
1!
1660
    }
1✔
1661
}
3✔
1662

1663
#if REALM_ENABLE_AUTH_TESTS
1664
static void generate_event(std::shared_ptr<Realm> realm, int call = 0)
1665
{
32✔
1666
    auto table = realm->read_group().get_table("class_object");
32✔
1667
    auto audit = realm->audit_context();
32✔
1668

1669
    realm->begin_transaction();
32✔
1670
    table->create_object_with_primary_key(call + 1).set_all(2);
32✔
1671
    realm->commit_transaction();
32✔
1672

1673
    auto scope = audit->begin_scope("scope");
32✔
1674
    Object(realm, table->get_object(call));
32✔
1675
    audit->end_scope(scope, assert_no_error);
32✔
1676
}
32✔
1677

1678
TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") {
14✔
1679
    // None of these tests need a deterministic clock, but the server rounding
1680
    // timestamps to milliseconds can result in events not having monotonically
1681
    // increasing timestamps with an actual clock.
1682
    TestClock clock;
14✔
1683

1684
    const Schema schema{
14✔
1685
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
14✔
1686
        {"AuditEvent",
14✔
1687
         {
14✔
1688
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
14✔
1689
             {"timestamp", PropertyType::Date},
14✔
1690
             {"activity", PropertyType::String},
14✔
1691
             {"event", PropertyType::String | PropertyType::Nullable},
14✔
1692
             {"data", PropertyType::String | PropertyType::Nullable},
14✔
1693
             {"metadata 1", PropertyType::String | PropertyType::Nullable},
14✔
1694
             {"metadata 2", PropertyType::String | PropertyType::Nullable},
14✔
1695
         }}};
14✔
1696
    const Schema no_audit_event_schema{
14✔
1697
        {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}};
14✔
1698

1699
    auto app_create_config = default_app_config();
14✔
1700
    app_create_config.schema = schema;
14✔
1701
    app_create_config.dev_mode_enabled = false;
14✔
1702
    TestAppSession session = create_app(app_create_config);
14✔
1703

1704
    SyncTestFile config(session.app()->current_user(), bson::Bson("default"));
14✔
1705
    config.automatic_change_notifications = false;
14✔
1706
    config.schema = schema;
14✔
1707
    config.audit_config = std::make_shared<AuditConfig>();
14✔
1708
    config.audit_config->logger = audit_logger;
14✔
1709
    config.audit_config->base_file_path = session.app()->config().base_file_path;
14✔
1710

1711
    auto expect_error = [&](auto&& config, auto&& fn) -> SyncError {
14✔
1712
        std::mutex mutex;
3✔
1713
        util::Optional<SyncError> error;
3✔
1714
        config.audit_config->sync_error_handler = [&](SyncError e) {
3✔
1715
            std::lock_guard lock(mutex);
3✔
1716
            error = e;
3✔
1717
        };
3✔
1718

1719
        auto realm = Realm::get_shared_realm(config);
3✔
1720
        fn(realm, 0);
3✔
1721

1722
        timed_wait_for(
3✔
1723
            [&] {
3,310✔
1724
                std::lock_guard lock(mutex);
3,310✔
1725
                return (bool)error;
3,310✔
1726
            },
3,310✔
1727
            std::chrono::seconds(30));
3✔
1728
        REQUIRE(bool(error));
3!
1729
        return *error;
3✔
1730
    };
3✔
1731

1732
    SECTION("basic functionality") {
14✔
1733
        auto realm = Realm::get_shared_realm(config);
1✔
1734
        realm->sync_session()->close();
1✔
1735
        generate_event(realm);
1✔
1736

1737
        auto events = get_audit_events_from_baas(session, *session.app()->current_user(), 1);
1✔
1738
        REQUIRE(events.size() == 1);
1!
1739
        REQUIRE(events[0].activity == "scope");
1!
1740
        REQUIRE(events[0].event == "read");
1!
1741
        REQUIRE(!events[0].timestamp.is_null()); // FIXME
1!
1742
        REQUIRE(events[0].data == json({{"type", "object"}, {"value", {{{"_id", 1}, {"value", 2}}}}}));
1!
1743
    }
1✔
1744

1745
    SECTION("different user from parent Realm") {
14✔
1746
        auto sync_user = session.app()->current_user();
1✔
1747
        create_user_and_log_in(session.app());
1✔
1748
        auto audit_user = session.app()->current_user();
1✔
1749
        config.audit_config->audit_user = audit_user;
1✔
1750
        auto realm = Realm::get_shared_realm(config);
1✔
1751
        // If audit uses the sync user this'll make it fail as that user is logged out
1752
        sync_user->log_out();
1✔
1753

1754
        generate_event(realm);
1✔
1755
        REQUIRE(get_audit_events_from_baas(session, *audit_user, 1).size() == 1);
1!
1756
    }
1✔
1757

1758
    SECTION("different app from parent Realm") {
14✔
1759
        auto audit_user = session.app()->current_user();
1✔
1760

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

1771
        auto realm = Realm::get_shared_realm(config);
1✔
1772
        generate_event(realm);
1✔
1773
        REQUIRE(get_audit_events_from_baas(session, *audit_user, 1).size() == 1);
1!
1774
    }
1✔
1775

1776
    SECTION("valid metadata properties") {
14✔
1777
        auto realm = Realm::get_shared_realm(config);
1✔
1778
        generate_event(realm, 0);
1✔
1779
        realm->audit_context()->update_metadata({{"metadata 1", "value 1"}});
1✔
1780
        generate_event(realm, 1);
1✔
1781
        realm->audit_context()->update_metadata({{"metadata 2", "value 2"}});
1✔
1782
        generate_event(realm, 2);
1✔
1783
        realm->audit_context()->update_metadata({{"metadata 1", "value 3"}, {"metadata 2", "value 4"}});
1✔
1784
        generate_event(realm, 3);
1✔
1785

1786
        using Metadata = std::map<std::string, std::string>;
1✔
1787
        auto events = get_audit_events_from_baas(session, *session.app()->current_user(), 4);
1✔
1788
        REQUIRE(events[0].metadata.empty());
1!
1789
        REQUIRE(events[1].metadata == Metadata({{"metadata 1", "value 1"}}));
1!
1790
        REQUIRE(events[2].metadata == Metadata({{"metadata 2", "value 2"}}));
1!
1791
        REQUIRE(events[3].metadata == Metadata({{"metadata 1", "value 3"}, {"metadata 2", "value 4"}}));
1!
1792
    }
1✔
1793

1794
    SECTION("invalid metadata properties") {
14✔
1795
        config.audit_config->metadata = {{"invalid key", "value"}};
1✔
1796
        auto error = expect_error(config, generate_event);
1✔
1797
        REQUIRE_THAT(error.status.reason(), StartsWith("Invalid schema change"));
1✔
1798
        REQUIRE(error.is_fatal);
1!
1799
    }
1✔
1800

1801
    SECTION("removed sync user") {
14✔
1802
        create_user_and_log_in(session.app());
1✔
1803
        auto audit_user = session.app()->current_user();
1✔
1804
        config.audit_config->audit_user = audit_user;
1✔
1805
        auto realm = Realm::get_shared_realm(config);
1✔
1806
        session.app()->remove_user(audit_user, nullptr);
1✔
1807

1808
        auto audit = realm->audit_context();
1✔
1809
        auto scope = audit->begin_scope("scope");
1✔
1810
        realm->begin_transaction();
1✔
1811
        auto table = realm->read_group().get_table("class_object");
1✔
1812
        table->create_object_with_primary_key(1).set_all(2);
1✔
1813
        realm->commit_transaction();
1✔
1814

1815
        audit->end_scope(scope, [&](auto error) {
1✔
1816
            REQUIRE(error);
1!
1817
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "user has been removed");
1✔
1818
        });
1✔
1819
        audit->wait_for_completion();
1✔
1820
    }
1✔
1821

1822
    SECTION("AuditEvent missing from server schema") {
14✔
1823
        app_create_config.schema = no_audit_event_schema;
1✔
1824
        TestAppSession session_2 = create_app(app_create_config);
1✔
1825
        SyncTestFile config(session_2.app()->current_user(), bson::Bson("default"));
1✔
1826
        config.schema = no_audit_event_schema;
1✔
1827
        config.audit_config = std::make_shared<AuditConfig>();
1✔
1828
        config.audit_config->base_file_path = session.app()->config().base_file_path;
1✔
1829

1830
        auto error = expect_error(config, generate_event);
1✔
1831
        REQUIRE_THAT(error.status.reason(), StartsWith("Invalid schema change"));
1✔
1832
        REQUIRE(error.is_fatal);
1!
1833
    }
1✔
1834

1835
    SECTION("incoming changesets are discarded") {
14✔
1836
        app::MongoClient remote_client = session.app()->current_user()->mongo_client("BackingDB");
2✔
1837
        app::MongoDatabase db = remote_client.db(session.app_session().config.mongo_dbname);
2✔
1838
        app::MongoCollection collection = db["AuditEvent"];
2✔
1839

1840
        SECTION("objects deleted on server") {
2✔
1841
            // Because EraseObject is idempotent, this case actually just works
1842
            // without any special logic.
1843
            auto delete_one = [&] {
10✔
1844
                uint64_t deleted = 0;
10✔
1845
                while (deleted == 0) {
96✔
1846
                    collection.delete_one({},
86✔
1847
                                          [&](util::Optional<uint64_t>&& count, util::Optional<app::AppError> error) {
86✔
1848
                                              REQUIRE_FALSE(error);
86!
1849
                                              deleted = *count;
86✔
1850
                                          });
86✔
1851
                    if (deleted == 0) {
86✔
1852
                        millisleep(100); // slow down the number of retries
76✔
1853
                    }
76✔
1854
                }
86✔
1855
            };
10✔
1856

1857
            auto realm = Realm::get_shared_realm(config);
1✔
1858
            for (int i = 0; i < 10; ++i) {
11✔
1859
                generate_event(realm, i);
10✔
1860
                delete_one();
10✔
1861
            }
10✔
1862
        }
1✔
1863

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

1885
            auto realm = Realm::get_shared_realm(config);
1✔
1886
            for (int i = 0; i < 10; ++i) {
11✔
1887
                generate_event(realm, i);
10✔
1888
                update_one();
10✔
1889
            }
10✔
1890
        }
1✔
1891
    }
2✔
1892

1893
    SECTION("flexible sync") {
14✔
1894
        app::FLXSyncTestHarness harness("audit", {schema});
3✔
1895
        create_user_and_log_in(harness.app());
3✔
1896

1897
        SECTION("auditing a flexible sync realm without specifying an audit user throws an exception") {
3✔
1898
            SyncTestFile config(harness.app()->current_user(), schema, SyncConfig::FLXSyncEnabled{});
1✔
1899
            config.audit_config = std::make_shared<AuditConfig>();
1✔
1900
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "partition-based sync");
1✔
1901
        }
1✔
1902

1903
        SECTION("auditing with a flexible sync user reports a sync error") {
3✔
1904
            config.audit_config->audit_user = harness.app()->current_user();
1✔
1905
            auto error = expect_error(config, generate_event);
1✔
1906
            REQUIRE_THAT(error.status.reason(),
1✔
1907
                         Catch::Matchers::ContainsSubstring(
1✔
1908
                             "Client connected using partition-based sync when app is using flexible sync"));
1✔
1909
            REQUIRE(error.is_fatal);
1!
1910
        }
1✔
1911

1912
        SECTION("auditing a flexible sync realm with a pbs audit user works") {
3✔
1913
            config.audit_config->audit_user = config.sync_config->user;
1✔
1914
            config.sync_config->user = harness.app()->current_user();
1✔
1915
            config.sync_config->flx_sync_requested = true;
1✔
1916
            config.sync_config->partition_value.clear();
1✔
1917
            config.schema_version = 0;
1✔
1918

1919
            auto realm = Realm::get_shared_realm(config);
1✔
1920
            {
1✔
1921
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
1✔
1922
                mut_subs.insert_or_assign(Query(realm->read_group().get_table("class_object")));
1✔
1923
                std::move(mut_subs).commit();
1✔
1924
            }
1✔
1925

1926
            realm->sync_session()->force_close();
1✔
1927
            generate_event(realm, 0);
1✔
1928
            get_audit_events_from_baas(session, *session.app()->current_user(), 1);
1✔
1929
        }
1✔
1930
    }
3✔
1931

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

1942
        audit_user->log_out();
1✔
1943
        generate_event(realm);
1✔
1944
        log_in_user(session.app(), creds);
1✔
1945

1946
        REQUIRE(get_audit_events_from_baas(session, *sync_user, 1).size() == 1);
1!
1947
    }
1✔
1948

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

1960
        auto audit = realm->audit_context();
1✔
1961
        REQUIRE(audit);
1!
1962

1963
        // Set a small shard size so that we don't have to write an absurd
1964
        // amount of data to test this
1965
        audit_test_hooks::set_maximum_shard_size(32 * 1024);
1✔
1966
        auto cleanup = util::make_scope_exit([]() noexcept {
1✔
1967
            audit_test_hooks::set_maximum_shard_size(256 * 1024 * 1024);
1✔
1968
        });
1✔
1969

1970
        realm->begin_transaction();
1✔
1971
        auto table = realm->read_group().get_table("class_object");
1✔
1972
        std::vector<Obj> objects;
1✔
1973
        for (int i = 0; i < 2000; ++i)
2,001✔
1974
            objects.push_back(table->create_object_with_primary_key(i));
2,000✔
1975
        realm->commit_transaction();
1✔
1976

1977
        // Write a lot of audit scopes while unable to sync
1978
        for (int i = 0; i < 50; ++i) {
51✔
1979
            auto scope = audit->begin_scope(util::format("scope %1", i));
50✔
1980
            Results(realm, table->where()).snapshot();
50✔
1981
            audit->end_scope(scope, assert_no_error);
50✔
1982
        }
50✔
1983
        audit->wait_for_completion();
1✔
1984

1985
        // Client file idents aren't reread while a session is active, so we need
1986
        // to close all of the open audit Realms awaiting upload
1987
        realm->close();
1✔
1988
        realm = nullptr;
1✔
1989
        auto sync_manager = session.sync_manager();
1✔
1990
        for (auto& session : sync_manager->get_all_sessions()) {
1✔
1991
            session->shutdown_and_wait();
1✔
1992
        }
1✔
1993

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

2008
        // Log the user back in and reopen the parent Realm to start trying to upload the audit data
2009
        log_in_user(session.app(), creds);
1✔
2010
        realm = Realm::get_shared_realm(config);
1✔
2011
        audit = realm->audit_context();
1✔
2012
        REQUIRE(audit);
1!
2013
        audit->wait_for_uploads();
1✔
2014

2015
        auto events = get_audit_events_from_baas(session, *sync_user, 50);
1✔
2016
        REQUIRE(events.size() == 50);
1!
2017
        for (int i = 0; i < 50; ++i) {
51✔
2018
            REQUIRE(events[i].activity == util::format("scope %1", i));
50!
2019
        }
50✔
2020
    }
1✔
2021

2022
#if 0 // This test takes ~10 minutes to run
2023
    SECTION("large audit scope") {
2024
        auto realm = Realm::get_shared_realm(config);
2025
        auto table = realm->read_group().get_table("class_object");
2026
        auto audit = realm->audit_context();
2027

2028
        realm->begin_transaction();
2029
        auto obj1 = table->create_object_with_primary_key(1);
2030
        auto obj2 = table->create_object_with_primary_key(2);
2031
        realm->commit_transaction();
2032

2033
        auto scope = audit->begin_scope("large");
2034
        for (int i = 0; i < 150'000; ++i) {
2035
            Object(realm, obj1);
2036
            Object(realm, obj2);
2037
        }
2038
        audit->end_scope(scope, assert_no_error);
2039

2040
        REQUIRE(get_audit_events_from_baas(session, *session.app()->current_user(), 300'000).size() == 300'000);
2041
    }
2042
#endif
2043
}
14✔
2044
#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