code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"
#include "model/misc/sequencer.h"
#include "model/cluster.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("remove peer", "[model]") {
    test::init_logging();

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device);

    auto &blocks_map = cluster->get_blocks();
    auto builder = diff_builder_t(*cluster);
    REQUIRE(builder.upsert_folder("1234-5678", "some/path", "my-label").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());

    SECTION("1 file, 1 folder, 1 block") {
        auto bi = proto::BlockInfo();
        proto::set_hash(bi, utils::sha256_digest(as_bytes("12345")).value());
        proto::set_size(bi, 5);
        auto block = block_info_t::create(bi).assume_value();
        blocks_map.put(block);

        auto folder = cluster->get_folders().by_id("1234-5678");
        auto folder_info = folder->get_folder_infos().by_device(*peer_device);
        REQUIRE(folder_info);

        proto::FileInfo pr_fi;
        proto::set_name(pr_fi, "a.txt");
        proto::set_block_size(pr_fi, 5ul);
        proto::set_size(pr_fi, 5ul);
        auto &v = proto::get_version(pr_fi);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

        auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
        auto &b1 = proto::add_blocks(pr_fi);
        proto::set_hash(b1, as_bytes("123"));
        proto::set_size(b1, 5);

        auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, folder_info).value();
        fi->assign_block(block, 0);
        folder_info->get_file_infos().put(fi);

        REQUIRE(cluster->get_blocks().size() == 1);
        REQUIRE(cluster->get_devices().size() == 2);

        REQUIRE(builder.remove_peer(*peer_device).apply());
        CHECK(cluster->get_blocks().size() == 0);
        CHECK(cluster->get_devices().size() == 1);
        CHECK(!folder->is_shared_with(*peer_device));
        CHECK(devices.size() == 1);
    }

    SECTION("3 files, 2 shared files, 2 shared folders, 3 block, 2 shared blocks") {
        REQUIRE(builder.upsert_folder("1234", "some-2/path-2", "my-label-2").apply());
        REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234").apply());

        auto b1 = [&]() {
            auto bi = proto::BlockInfo();
            proto::set_hash(bi, utils::sha256_digest(as_bytes("1")).value());
            proto::set_size(bi, 5);
            auto block = block_info_t::create(bi).assume_value();
            blocks_map.put(block);
            return block;
        }();
        auto b2 = [&]() {
            auto bi = proto::BlockInfo();
            proto::set_hash(bi, utils::sha256_digest(as_bytes("2")).value());
            proto::set_size(bi, 5);
            auto block = block_info_t::create(bi).assume_value();
            blocks_map.put(block);
            return block;
        }();
        auto b3 = [&]() {
            auto bi = proto::BlockInfo();
            proto::set_hash(bi, utils::sha256_digest(as_bytes("3")).value());
            proto::set_size(bi, 5);
            auto block = block_info_t::create(bi).assume_value();
            blocks_map.put(block);
            return block;
        }();

        auto f1 = cluster->get_folders().by_id("1234-5678");
        auto fi_1 = f1->get_folder_infos().by_device(*peer_device);
        REQUIRE(fi_1);

        auto f2 = cluster->get_folders().by_id("1234");
        auto fi_2 = f2->get_folder_infos().by_device(*peer_device);
        REQUIRE(fi_2);

        auto file_1 = [&]() {
            proto::FileInfo pr_fi;
            proto::set_name(pr_fi, "a.txt");
            proto::set_block_size(pr_fi, 5ul);
            proto::set_size(pr_fi, 5ul);
            auto &v = proto::get_version(pr_fi);
            proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

            auto b_hash = utils::sha256_digest(as_bytes("1")).value();
            auto &b = proto::add_blocks(pr_fi);
            proto::set_hash(b, b_hash);
            proto::set_size(b, 5);

            auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, fi_1).value();
            fi->assign_block(b1, 0);
            fi_1->get_file_infos().put(fi);
            return fi;
        }();

        auto file_2 = [&]() {
            proto::FileInfo pr_fi;
            proto::set_name(pr_fi, "b.txt");
            proto::set_block_size(pr_fi, 5ul);
            proto::set_size(pr_fi, 10ul);
            auto &v = proto::get_version(pr_fi);
            proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

            auto b1_hash = utils::sha256_digest(as_bytes("2")).value();
            auto &pr_b1 = proto::add_blocks(pr_fi);
            proto::set_hash(pr_b1, b1_hash);
            proto::set_size(pr_b1, 5);
            proto::set_offset(pr_b1, 0);

            auto b2_hash = utils::sha256_digest(as_bytes("1")).value();
            auto &pr_b2 = proto::add_blocks(pr_fi);
            proto::set_hash(pr_b2, b2_hash);
            proto::set_size(pr_b2, 5);
            proto::set_offset(pr_b2, 0);

            auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, fi_2).value();
            fi->assign_block(b2, 0);
            fi->assign_block(b1, 1);
            fi_2->get_file_infos().put(fi);
            return fi;
        }();

        REQUIRE(cluster->get_blocks().size() == 3);
        REQUIRE(cluster->get_devices().size() == 2);

        REQUIRE(builder.remove_peer(*peer_device).apply());
        CHECK(cluster->get_blocks().size() == 1);
        CHECK(cluster->get_devices().size() == 1);
        CHECK(!f1->is_shared_with(*peer_device));
        CHECK(!f2->is_shared_with(*peer_device));
        CHECK(devices.size() == 1);
    }

    SECTION("unknown folders are removed") {
        db::PendingFolder db_pf;
        auto &db_f = db::get_folder(db_pf);
        db::set_id(db_f, "1234");
        auto &db_fi = db::get_folder_info(db_pf);
        db::set_max_sequence(db_fi, 5);
        db::set_index_id(db_fi, 10);

        auto uf = pending_folder_t::create(sequencer->next_uuid(), db_pf, peer_device->device_id()).value();
        auto &unknown_folders = cluster->get_pending_folders();
        unknown_folders.put(uf);
        REQUIRE(builder.remove_peer(*peer_device).apply());
        CHECK(devices.size() == 1);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"

#include "model/cluster.h"
#include "model/diff/contact/ignored_connected.h"
#include "model/diff/contact/unknown_connected.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("unknown device connected", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &devices = cluster->get_devices();
    devices.put(my_device);

    db::SomeDevice db_device;
    db::set_name(db_device, "a name");
    auto buider = diff_builder_t(*cluster);
    REQUIRE(buider.add_unknown_device(peer_id, db_device).apply());

    db::set_name(db_device, "a name-2");
    auto diff = model::diff::cluster_diff_ptr_t{};
    diff = new model::diff::contact::unknown_connected_t(*cluster, peer_id, db_device);
    REQUIRE(diff->apply(*cluster, get_apply_controller()));

    REQUIRE(cluster->get_pending_devices().size() == 1);
    auto unknown = cluster->get_pending_devices().by_sha256(peer_id.get_sha256());
    REQUIRE(unknown);
    CHECK(unknown->get_name() == "a name-2");
}

TEST_CASE("unknown device is removed when connecting to it ", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &devices = cluster->get_devices();
    devices.put(my_device);

    db::SomeDevice db_device;
    db::set_name(db_device, "a name");
    auto buider = diff_builder_t(*cluster);
    REQUIRE(buider.add_unknown_device(peer_id, db_device).apply());
    REQUIRE(cluster->get_pending_devices().size() == 1);
    REQUIRE(cluster->get_devices().size() == 1);

    REQUIRE(buider.update_peer(peer_id).apply());
    CHECK(cluster->get_pending_devices().size() == 0);
    CHECK(cluster->get_devices().size() == 2);
}

TEST_CASE("ignored device connected", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &devices = cluster->get_devices();
    devices.put(my_device);

    db::SomeDevice db_device;
    db::set_name(db_device, "a name");
    auto buider = diff_builder_t(*cluster);
    REQUIRE(buider.add_ignored_device(peer_id, db_device).apply());
    auto &ignored_devices = cluster->get_ignored_devices();
    auto ignored_device = ignored_devices.by_sha256(peer_id.get_sha256());

    auto diff = model::diff::cluster_diff_ptr_t{};
    diff = new model::diff::contact::ignored_connected_t(*cluster, peer_id, db_device);
    REQUIRE(diff->apply(*cluster, get_apply_controller()));
}

TEST_CASE("ignored device is removed when connecting to it ", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &devices = cluster->get_devices();
    devices.put(my_device);

    db::SomeDevice db_device;
    db::set_name(db_device, "a name");
    auto buider = diff_builder_t(*cluster);
    REQUIRE(buider.add_ignored_device(peer_id, db_device).apply());
    REQUIRE(cluster->get_ignored_devices().size() == 1);
    REQUIRE(cluster->get_devices().size() == 1);

    REQUIRE(buider.update_peer(peer_id).apply());
    CHECK(cluster->get_ignored_devices().size() == 0);
    CHECK(cluster->get_devices().size() == 2);
}

TEST_CASE("unknown device is removed adding the same ignored device", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &devices = cluster->get_devices();
    devices.put(my_device);

    db::SomeDevice db_device;
    db::set_name(db_device, "a name");
    auto buider = diff_builder_t(*cluster);
    REQUIRE(buider.add_unknown_device(peer_id, db_device).apply());
    REQUIRE(cluster->get_pending_devices().size() == 1);
    REQUIRE(cluster->get_ignored_devices().size() == 0);
    REQUIRE(cluster->get_devices().size() == 1);

    REQUIRE(buider.add_ignored_device(peer_id, db_device).apply());
    REQUIRE(cluster->get_pending_devices().size() == 0);
    REQUIRE(cluster->get_ignored_devices().size() == 1);
    REQUIRE(cluster->get_devices().size() == 1);
}

repo_name: basiliscos/syncspirit

license: mit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"

#include "model/cluster.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("remove folder", "[model]") {
    test::init_logging();

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto my_device = device_ptr_t{};
    my_device = new model::local_device_t(my_id, "my-device", "my-device");
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device);

    auto &blocks_map = cluster->get_blocks();
    auto builder = diff_builder_t(*cluster);
    REQUIRE(builder.upsert_folder("f1", "some/path-1", "my-label-1")
                .upsert_folder("f2", "some/path-2", "my-label-2")
                .apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "f1").apply());

    auto b1 = [&]() {
        auto bi = proto::BlockInfo();
        proto::set_hash(bi, utils::sha256_digest(as_bytes("1")).value());
        proto::set_size(bi, 5);
        auto block = block_info_t::create(bi).assume_value();
        blocks_map.put(block);
        return block;
    }();
    auto b2 = [&]() {
        auto bi = proto::BlockInfo();
        proto::set_hash(bi, utils::sha256_digest(as_bytes("2")).value());
        proto::set_size(bi, 5);
        auto block = block_info_t::create(bi).assume_value();
        blocks_map.put(block);
        return block;
    }();
    auto b3 = [&]() {
        auto bi = proto::BlockInfo();
        proto::set_hash(bi, utils::sha256_digest(as_bytes("3")).value());
        proto::set_size(bi, 5);
        auto block = block_info_t::create(bi).assume_value();
        blocks_map.put(block);
        return block;
    }();

    auto f1 = cluster->get_folders().by_id("f1");
    auto f2 = cluster->get_folders().by_id("f2");
    auto fi_1_peer = f1->get_folder_infos().by_device(*peer_device);
    auto fi_1_mine = f1->get_folder_infos().by_device(*my_device);
    auto fi_2_mine = f2->get_folder_infos().by_device(*my_device);

    auto file_1 = [&]() {
        proto::FileInfo pr_fi;
        proto::set_name(pr_fi, "a.txt");
        proto::set_block_size(pr_fi, 5ul);
        proto::set_size(pr_fi, 5ul);
        auto &v = proto::get_version(pr_fi);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

        auto b_hash = utils::sha256_digest(as_bytes("1")).value();
        auto &b = proto::add_blocks(pr_fi);
        proto::set_hash(b, b_hash);
        proto::set_size(b, 5);

        auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, fi_1_peer).value();
        fi->assign_block(b1, 0);
        fi_1_peer->get_file_infos().put(fi);
        return fi;
    }();

    auto file_2 = [&]() {
        proto::FileInfo pr_fi;
        proto::set_name(pr_fi, "b.txt");
        proto::set_block_size(pr_fi, 5ul);
        proto::set_size(pr_fi, 5ul);
        auto &v = proto::get_version(pr_fi);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

        auto b_hash = utils::sha256_digest(as_bytes("2")).value();
        auto &b = proto::add_blocks(pr_fi);
        proto::set_hash(b, b_hash);
        proto::set_size(b, 5);

        auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, fi_1_mine).value();
        fi->assign_block(b2, 0);
        fi_1_mine->get_file_infos().put(fi);
        return fi;
    }();

    auto file_3 = [&]() {
        proto::FileInfo pr_fi;
        proto::set_name(pr_fi, "c.txt");
        proto::set_block_size(pr_fi, 5ul);
        proto::set_size(pr_fi, 5ul);
        auto &v = proto::get_version(pr_fi);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));

        auto b_hash = utils::sha256_digest(as_bytes("3")).value();
        auto &b = proto::add_blocks(pr_fi);
        proto::set_hash(b, b_hash);
        proto::set_size(b, 5);

        auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, fi_2_mine).value();
        fi->assign_block(b3, 0);
        fi_2_mine->get_file_infos().put(fi);
        return fi;
    }();

    REQUIRE(cluster->get_blocks().size() == 3);
    REQUIRE(cluster->get_devices().size() == 2);
    REQUIRE(cluster->get_folders().size() == 2);

    SECTION("never exchanged info with a peer (index == 0") {
        REQUIRE(builder.remove_folder(*f1).remove_folder(*f2).apply());
        CHECK(cluster->get_pending_folders().size() == 0);
        CHECK(cluster->get_blocks().size() == 0);
        CHECK(cluster->get_folders().size() == 0);
    }

    SECTION("peer has seen the folder (index != 0)") {
        fi_1_peer->set_index(123);
        fi_1_peer->get_file_infos().put(file_1);
        REQUIRE(builder.remove_folder(*f1).remove_folder(*f2).apply());
        CHECK(cluster->get_pending_folders().size() == 1);
        CHECK(cluster->get_blocks().size() == 0);
        CHECK(cluster->get_folders().size() == 0);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"

#include "model/cluster.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("remove folder", "[model]") {
    test::init_logging();

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto my_device = device_ptr_t{};
    my_device = new model::local_device_t(my_id, "my-device", "my-device");
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto &blocks_map = cluster->get_blocks();
    auto sequencer = make_sequencer(4);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device);

    proto::FileInfo pr_fi;
    proto::set_name(pr_fi, "a.txt");
    proto::set_size(pr_fi, 5);
    auto &v = proto::get_version(pr_fi);
    proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 5));
    auto &b_1 = proto::add_blocks(pr_fi);
    proto::set_size(b_1, 5);
    proto::set_hash(b_1, utils::sha256_digest(as_bytes("12345")).value());

    auto b_2 = proto::BlockInfo();
    proto::set_size(b_2, 5);
    proto::set_hash(b_2, utils::sha256_digest(as_bytes("67890")).value());

    auto bi_1 = block_info_t::create(b_1).assume_value();
    blocks_map.put(bi_1);

    auto bi_2 = block_info_t::create(b_2).assume_value();
    blocks_map.put(bi_2);

    auto builder = diff_builder_t(*cluster);
    REQUIRE(builder.upsert_folder("f1", "some/path-1", "my-label-1").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "f1").apply());

    auto folder = cluster->get_folders().by_id("f1");
    auto folder_peer = folder->get_folder_infos().by_device(*peer_device);
    auto folder_my = folder->get_folder_infos().by_device(*my_device);
    auto &files_my = folder_my->get_file_infos();
    auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_fi, folder_peer).value();
    file_peer->assign_block(bi_1, 0);
    folder_peer->add_relaxed(file_peer);

    SECTION("local update") {
        SECTION("content changes") {
            auto pr_local = pr_fi;
            SECTION("new content") {
                proto::clear_blocks(pr_local);
                proto::add_blocks(pr_local) = b_2;
                REQUIRE(builder.local_update(folder->get_id(), pr_local).apply());
                REQUIRE(files_my.size() == 1);
                auto f_my = files_my.begin()->item;
                CHECK(f_my->get_blocks()[0] == bi_2);
                CHECK(f_my->get_version()->as_proto() != file_peer->get_version()->as_proto());
            }
            SECTION("identical content (aka importing)") {
                REQUIRE(builder.local_update(folder->get_id(), pr_local).apply());
                REQUIRE(files_my.size() == 1);
                auto f_my = files_my.begin()->item;
                CHECK(f_my->get_blocks()[0] == bi_1);
                CHECK(f_my->get_version()->as_proto() == file_peer->get_version()->as_proto());
            }
            SECTION("after suspending") {
                REQUIRE(builder.suspend(*folder).local_update(folder->get_id(), pr_local).apply());
                REQUIRE(files_my.size() == 0);
            }
            SECTION("after removing") {
                REQUIRE(builder.remove_folder(*folder).local_update(folder->get_id(), pr_local).apply());
                REQUIRE(files_my.size() == 0);
            }
        }
    }

    SECTION("remote copy") {
        SECTION("all ok") {
            REQUIRE(builder.remote_copy(*file_peer).apply());
            REQUIRE(files_my.size() == 1);
        }
        SECTION("after unsharing") {
            REQUIRE(builder.unshare_folder(*folder_peer).remote_copy(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
        SECTION("after suspending") {
            REQUIRE(builder.suspend(*folder).remote_copy(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
        SECTION("after removing") {
            REQUIRE(builder.remove_folder(*folder).remote_copy(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
    }

    SECTION("remote win") {
        auto pr_fi_local = pr_fi;
        auto &v_l = proto::get_version(pr_fi_local);
        auto &c_l = proto::get_counters(v_l, 0);
        proto::set_value(c_l, 3);

        SECTION("all ok") {
            REQUIRE(builder.advance(*file_peer).apply());
            REQUIRE(files_my.size() == 1);
        }
        SECTION("after unsharing") {
            REQUIRE(builder.unshare_folder(*folder_peer).advance(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
        SECTION("after suspending") {
            REQUIRE(builder.suspend(*folder).advance(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
        SECTION("after removing") {
            REQUIRE(builder.remove_folder(*folder).advance(*file_peer).apply());
            REQUIRE(files_my.size() == 0);
        }
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/cluster.h"
#include "model/misc/file_iterator.h"
#include "model/misc/sequencer.h"
#include "diff-builder.h"
#include <set>

using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;

using R = file_iterator_t::result_t;
using A = advance_action_t;

TEST_CASE("file iterator, single folder", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = cluster->get_folders().by_id(folder->get_id())->get_folder_infos();
    REQUIRE(folder_infos.size() == 2u);

    auto peer_folder = folder_infos.by_device(*peer_device);
    auto &peer_files = peer_folder->get_file_infos();

    auto my_folder = folder_infos.by_device(*my_device);
    auto &my_files = my_folder->get_file_infos();

    auto file_iterator = peer_device->create_iterator(*cluster);

    SECTION("emtpy folders (1)") { CHECK(file_iterator->next() == R{nullptr, A::ignore}); }

    REQUIRE(builder.configure_cluster(peer_id.get_sha256())
                .add(peer_id.get_sha256(), folder->get_id(), 123, 10u)
                .finish()
                .apply());

    SECTION("emtpy folders (2)") { CHECK(file_iterator->next() == R{nullptr, A::ignore}); }

    SECTION("cloning (empty files)") {
        SECTION("1 file") {
            SECTION("no local file") {
                auto pr_fi = proto::FileInfo();
                proto::set_name(pr_fi, "a.txt");
                proto::set_sequence(pr_fi, 10);
                auto &v = proto::get_version(pr_fi);
                proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));
                auto index_builder = builder.make_index(peer_id.get_sha256(), folder->get_id());

                SECTION("regular file") {
                    auto ec = index_builder.add(pr_fi, peer_device).finish().apply();
                    REQUIRE(ec);

                    auto [f, action] = file_iterator->next();
                    REQUIRE(f);
                    CHECK(f->get_name() == "a.txt");
                    CHECK(!f->is_locked());
                    CHECK(action == A::remote_copy);

                    REQUIRE(builder.apply());
                    CHECK(file_iterator->next() == R{nullptr, A::ignore});

                    REQUIRE(builder.remote_copy(*f).apply());
                    CHECK(file_iterator->next() == R{nullptr, A::ignore});
                    CHECK(!f->is_locked());
                }
                SECTION("symblink") {
                    proto::set_symlink_target(pr_fi, "b.txt");
                    proto::set_type(pr_fi, proto::FileInfoType::SYMLINK);
                    auto ec = index_builder.add(pr_fi, peer_device).finish().apply();
                    REQUIRE(ec);

                    auto [f, action] = file_iterator->next();
                    REQUIRE(f);
                    CHECK(f->get_name() == "a.txt");
                    CHECK(!f->is_locked());
                    CHECK(action == A::remote_copy);
                }
            }
            SECTION("invalid file is ignored") {
                auto pr_fi = proto::FileInfo();
                proto::set_name(pr_fi, "a.txt");
                proto::set_sequence(pr_fi, 10);
                proto::set_invalid(pr_fi, true);
                REQUIRE(builder.apply());
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
            }

            SECTION("version checks") {
                auto pr_fi = proto::FileInfo();
                proto::set_name(pr_fi, "a.txt");
                proto::set_sequence(pr_fi, 10);
                auto &v = proto::get_version(pr_fi);
                auto &c_1 = proto::add_counters(v);
                proto::set_id(c_1, 1);
                proto::set_value(c_1, 5);

                builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish();

                SECTION("my version < peer version") {
                    proto::set_value(c_1, 3);
                    REQUIRE(builder.apply());

                    auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
                    my_file->mark_local();
                    my_files.put(my_file);

                    auto [f, action] = file_iterator->next();
                    REQUIRE(f);
                    CHECK(action == A::remote_copy);
                    CHECK(f->get_folder_info()->get_device() == peer_device.get());
                    CHECK(file_iterator->next() == R{nullptr, A::ignore});
                }

                SECTION("my version < peer version, but not scanned yet") {
                    REQUIRE(builder.apply());
                    proto::set_value(c_1, 3);

                    auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
                    my_files.put(my_file);

                    CHECK(file_iterator->next() == R{nullptr, A::ignore});
                }

                SECTION("my version > peer version") {
                    REQUIRE(builder.apply());
                    proto::set_value(c_1, 10);

                    auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
                    my_file->mark_local();
                    my_files.put(my_file);

                    CHECK(file_iterator->next() == R{nullptr, A::ignore});
                }

                SECTION("my version == peer version") {
                    REQUIRE(builder.apply());

                    auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
                    my_file->mark_local();
                    my_files.put(my_file);

                    CHECK(file_iterator->next() == R{nullptr, A::ignore});
                }
            }
        }
        SECTION("2+ files") {
            auto files = model::file_infos_map_t();
            auto pr_fi_1 = proto::FileInfo();
            proto::set_name(pr_fi_1, "a.txt");
            proto::set_sequence(pr_fi_1, 10ul);
            auto pr_fi_2 = proto::FileInfo();
            proto::set_name(pr_fi_2, "b.txt");
            proto::set_sequence(pr_fi_2, 11ul);
            builder.make_index(peer_id.get_sha256(), folder->get_id())
                .add(pr_fi_1, peer_device)
                .add(pr_fi_2, peer_device)
                .finish();
            REQUIRE(builder.apply());

            SECTION("both files are missing on my side") {
                auto [f, action] = file_iterator->next();
                REQUIRE(f);
                CHECK(action == A::remote_copy);
                CHECK((f->get_name() == "a.txt" || f->get_name() == "b.txt"));
                CHECK(!f->is_locked());
                files.put(f);
                REQUIRE(builder.apply());

                REQUIRE(builder.remote_copy(*f).apply());

                std::tie(f, action) = file_iterator->next();
                REQUIRE(f);
                CHECK((f->get_name() == "a.txt" || f->get_name() == "b.txt"));
                CHECK(!f->is_locked());
                CHECK(action == A::remote_copy);
                files.put(f);
                REQUIRE(builder.apply());
                REQUIRE(builder.remote_copy(*f).apply());

                CHECK(file_iterator->next() == R{nullptr, A::ignore});

                CHECK(files.by_name("a.txt"));
                CHECK(files.by_name("b.txt"));
            }
            SECTION("1 file is missing on my side") {
                auto peer_file = peer_files.by_name("a.txt");
                REQUIRE(peer_file);
                REQUIRE(builder.remote_copy(*peer_file).apply());
                auto [f, action] = file_iterator->next();
                REQUIRE(f);
                CHECK(action == A::remote_copy);
                CHECK(f->get_name() == "b.txt");
                CHECK(file_iterator->next() == R{nullptr, A::ignore});

                REQUIRE(builder.apply());
                CHECK(file_iterator->next() == R{nullptr, A::ignore});

                REQUIRE(builder.remote_copy(*f).apply());
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
                CHECK(!f->is_locked());
            }

            SECTION("0 files are missing on my side") {
                auto peer_file_1 = peer_files.by_name("a.txt");
                auto peer_file_2 = peer_files.by_name("b.txt");
                REQUIRE(builder.remote_copy(*peer_file_1).remote_copy(*peer_file_2).apply());
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
            }
            SECTION("new file in new peer update") {
                auto [f_1, action_1] = file_iterator->next();
                REQUIRE(f_1);
                CHECK(action_1 == A::remote_copy);
                CHECK((f_1->get_name() == "a.txt" || f_1->get_name() == "b.txt"));
                CHECK(!f_1->is_locked());
                files.put(f_1);

                auto [f_2, action_2] = file_iterator->next();
                REQUIRE(f_2);
                CHECK(action_2 == A::remote_copy);
                CHECK((f_2->get_name() == "a.txt" || f_2->get_name() == "b.txt"));
                CHECK(!f_2->is_locked());
                files.put(f_2);

                CHECK(files.by_name("a.txt"));
                CHECK(files.by_name("b.txt"));

                auto pr_fi_3 = proto::FileInfo();
                proto::set_name(pr_fi_3, "c.txt");
                proto::set_sequence(pr_fi_3, 12ul);
                auto ec = builder.make_index(peer_id.get_sha256(), folder->get_id())
                              .add(pr_fi_3, peer_device)
                              .finish()
                              .apply();

                auto [f_3, action_3] = file_iterator->next();
                REQUIRE(f_3);
                CHECK(action_3 == A::remote_copy);
                CHECK(f_3->get_name() == "c.txt");
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
            }
        }
    }

    SECTION("synchronizing non-empty files") {
        auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
        auto b1 = proto::BlockInfo();
        proto::set_hash(b1, b1_hash);
        proto::set_size(b1, 5);

        auto b2_hash = utils::sha256_digest(as_bytes("67890")).value();
        auto b2 = proto::BlockInfo();
        proto::set_hash(b2, b2_hash);
        proto::set_size(b2, 5);

        auto &blocks_map = cluster->get_blocks();
        blocks_map.put(block_info_t::create(b1).value());
        blocks_map.put(block_info_t::create(b2).value());

        SECTION("missing locally") {
            auto pr_fi = proto::FileInfo();
            proto::set_name(pr_fi, "a.txt");
            proto::set_sequence(pr_fi, 10);
            proto::set_size(pr_fi, proto::get_size(b1));
            proto::set_block_size(pr_fi, proto::get_size(b1));
            proto::add_blocks(pr_fi, b1);

            REQUIRE(
                builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());

            SECTION("folder is suspended") {
                REQUIRE(builder.suspend(*folder).apply());
                auto [f, action] = file_iterator->next();
                CHECK(!f);
                CHECK(action == A::ignore);
            }

            SECTION("folder is not suspended") {
                auto [f, action] = file_iterator->next();
                REQUIRE(f);
                CHECK(action == A::remote_copy);
                CHECK(f->get_name() == "a.txt");
                CHECK(!f->is_locked());
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
            }
        }

        SECTION("have local, but outdated") {
            auto pr_fi = proto::FileInfo();
            proto::set_name(pr_fi, "a.txt");
            proto::set_sequence(pr_fi, 10);
            proto::set_size(pr_fi, proto::get_size(b1));
            proto::set_block_size(pr_fi, proto::get_size(b1));
            proto::add_blocks(pr_fi, b1);

            auto &v = proto::get_version(pr_fi);
            proto::add_counters(v, proto::Counter(1, 1));

            auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
            my_files.put(my_file);

            proto::add_counters(v, proto::Counter(2, 2));
            REQUIRE(
                builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());

            SECTION("has been scanned") {
                my_file->mark_local();

                auto [f, action] = file_iterator->next();
                REQUIRE(f);
                CHECK(action == A::remote_copy);
                CHECK(f->get_name() == "a.txt");
                CHECK(file_iterator->next() == R{nullptr, A::ignore});
                REQUIRE(builder.apply());
            }

            SECTION("has not bee scanned") { CHECK(file_iterator->next() == R{nullptr, A::ignore}); }
        }

        SECTION("have local, local is newer") {
            auto pr_fi = proto::FileInfo();
            proto::set_name(pr_fi, "a.txt");
            proto::set_sequence(pr_fi, 10);
            proto::set_size(pr_fi, proto::get_size(b1));
            proto::set_block_size(pr_fi, proto::get_size(b1));
            proto::add_blocks(pr_fi, b1);

            auto &v = proto::get_version(pr_fi);
            proto::add_counters(v, proto::Counter(1, 1));

            builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish();

            proto::add_counters(v, proto::Counter(2, 2));
            auto my_file = file_info_t::create(sequencer->next_uuid(), pr_fi, my_folder).value();
            my_files.put(my_file);

            REQUIRE(builder.apply());
            CHECK(file_iterator->next() == R{nullptr, A::ignore});
        }

        SECTION("peer file is unreacheable") {
            auto pr_fi = proto::FileInfo();
            proto::set_name(pr_fi, "a.txt");
            proto::set_sequence(pr_fi, 10);
            proto::set_size(pr_fi, proto::get_size(b1));
            proto::set_block_size(pr_fi, proto::get_size(b1));
            proto::add_blocks(pr_fi, b1);

            REQUIRE(
                builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());
            auto f = peer_files.by_name(proto::get_name(pr_fi));
            REQUIRE(builder.remote_copy(*f).mark_reacheable(f, false).apply());

            CHECK(file_iterator->next() == R{nullptr, A::ignore});
        }
    }
}

TEST_CASE("file iterator for 2 folders", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = cluster->get_folders().by_id(folder->get_id())->get_folder_infos();
    REQUIRE(folder_infos.size() == 2u);

    auto peer_folder = folder_infos.by_device(*peer_device);
    auto my_folder = folder_infos.by_device(*my_device);
    auto file_iterator = peer_device->create_iterator(*cluster);
    auto sha256 = peer_id.get_sha256();

    REQUIRE(builder.upsert_folder("1234", "/", "my-label-1").upsert_folder("5678", "/", "my-label-2").apply());
    REQUIRE(builder.share_folder(sha256, "1234").share_folder(sha256, "5678").apply());
    auto folder1 = folders.by_id("1234");
    auto folder2 = folders.by_id("5678");

    REQUIRE(builder.configure_cluster(sha256)
                .add(sha256, "1234", 123, 10u)
                .add(sha256, "5678", 1234u, 11)
                .finish()
                .apply());

    auto pr_fi_1 = proto::FileInfo();
    proto::set_name(pr_fi_1, "a.txt");
    proto::set_sequence(pr_fi_1, 10);

    auto pr_fi_2 = proto::FileInfo();
    proto::set_name(pr_fi_2, "b.txt");
    proto::set_sequence(pr_fi_2, 11);

    using set_t = std::set<std::string_view>;

    SECTION("cloning") {
        REQUIRE(
            builder.make_index(sha256, "1234").add(pr_fi_1, peer_device).add(pr_fi_2, peer_device).finish().apply());

        auto [f1, action1] = file_iterator->next();
        auto [f2, action2] = file_iterator->next();
        REQUIRE(f1);
        REQUIRE(f2);
        CHECK(action1 == A::remote_copy);
        CHECK(action2 == A::remote_copy);

        auto files = set_t{};
        files.emplace(f1->get_name());
        files.emplace(f2->get_name());

        CHECK((files == set_t{"a.txt", "b.txt"}));
        CHECK(file_iterator->next() == R{nullptr, A::ignore});
    }

    SECTION("syncing") {
        auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
        auto b1 = proto::BlockInfo();
        proto::set_hash(b1, b1_hash);
        proto::set_size(b1, 5);

        auto b2_hash = utils::sha256_digest(as_bytes("67890")).value();
        auto b2 = proto::BlockInfo();
        proto::set_hash(b2, b1_hash);
        proto::set_size(b2, 5);

        auto &blocks_map = cluster->get_blocks();
        blocks_map.put(block_info_t::create(b1).value());
        blocks_map.put(block_info_t::create(b2).value());

        proto::add_blocks(pr_fi_1, b1);
        proto::set_size(pr_fi_1, proto::get_size(b1));

        proto::add_blocks(pr_fi_2, b2);
        proto::set_size(pr_fi_2, proto::get_size(b2));

        using set_t = std::set<std::string_view>;
        REQUIRE(
            builder.make_index(sha256, "1234").add(pr_fi_1, peer_device).add(pr_fi_2, peer_device).finish().apply());

        auto files = set_t{};
        auto [f1, action1] = file_iterator->next();
        REQUIRE(f1);
        CHECK(action1 == A::remote_copy);
        files.emplace(f1->get_name());

        auto [f2, action2] = file_iterator->next();
        REQUIRE(f2);
        CHECK(action2 == A::remote_copy);
        files.emplace(f2->get_name());

        files.emplace(f1->get_name());
        files.emplace(f2->get_name());

        CHECK((files == set_t{"a.txt", "b.txt"}));
    }
}

TEST_CASE("file iterator, create, share, iterae, unshare, share, iterate", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto folder_infos = &folder->get_folder_infos();
    REQUIRE(folder_infos->size() == 2u);

    auto file_iterator = peer_device->create_iterator(*cluster);
    auto pr_fi = proto::FileInfo();
    proto::set_name(pr_fi, "a.txt");
    proto::set_sequence(pr_fi, 10);
    REQUIRE(builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());

    auto [f, action] = file_iterator->next();
    REQUIRE(f);
    CHECK(f->get_name() == "a.txt");
    CHECK(!f->is_locked());
    CHECK(action == A::remote_copy);

    REQUIRE(builder.apply());
    CHECK(file_iterator->next() == R{nullptr, A::ignore});
    REQUIRE(builder.remove_folder(*folder).apply());
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    folder = folders.by_id("1234-5678");
    folder_infos = &folder->get_folder_infos();
    REQUIRE(folder_infos->size() == 2u);
    REQUIRE(builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());

    std::tie(f, action) = file_iterator->next();
    REQUIRE(f);
    CHECK(action == A::remote_copy);
    CHECK(f->get_name() == "a.txt");
}

TEST_CASE("file pull order", "[model]") {
    using names_t = std::vector<std::string>;
    struct file_meta_t {
        std::string_view name;
        std::int64_t size;
        std::int64_t modified;
    };
    file_meta_t file_metas[5] = {
        {"0.txt", 0, 123}, {"1/a.txt", 5, 5000}, {"1/c.txt", 15, 4000}, {"1/d.txt", 10, 4500}, {"1/e.txt", 20, 6000},
    };

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto folder_infos = &folder->get_folder_infos();
    REQUIRE(folder_infos->size() == 2u);

    auto index = builder.make_index(peer_id.get_sha256(), folder->get_id());
    std::int64_t sequence = 10;
    for (auto &meta : file_metas) {
        auto pr = proto::FileInfo();
        proto::set_name(pr, meta.name);
        proto::set_size(pr, meta.size);
        proto::set_modified_s(pr, meta.modified);

        if (meta.size) {
            auto bytes_view = utils::bytes_view_t((unsigned char *)&file_metas, meta.size);
            auto hash = utils::sha256_digest(bytes_view).value();
            auto block = proto::BlockInfo();
            proto::set_hash(block, hash);
            proto::set_size(block, meta.size);
            proto::add_blocks(pr, std::move(block));
        }

        proto::set_sequence(pr, sequence++);
        index.add(pr, peer_device);
    }
    REQUIRE(index.finish().apply());

    SECTION("iteration anew") {
        auto iterate = [&]() -> names_t {
            auto file_iterator = peer_device->create_iterator(*cluster);
            auto names = names_t();
            while (true) {
                auto [fi, action] = file_iterator->next();
                if (!fi) {
                    break;
                };
                CHECK(action == A::remote_copy);
                names.emplace_back(std::string(fi->get_name()));
            }
            return names;
        };

        auto &pull_order = ((model::folder_data_t *)folder.get())->access<test::to::pull_order>();
        SECTION("defaul order (alphabetic)") {
            auto expected = names_t{"0.txt", "1/a.txt", "1/c.txt", "1/d.txt", "1/e.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("random (aka alphabetic)") {
            pull_order = db::PullOrder::random;
            auto expected = names_t{"0.txt", "1/a.txt", "1/c.txt", "1/d.txt", "1/e.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("alphabetic") {
            pull_order = db::PullOrder::alphabetic;
            auto expected = names_t{"0.txt", "1/a.txt", "1/c.txt", "1/d.txt", "1/e.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("smallest") {
            pull_order = db::PullOrder::smallest;
            auto expected = names_t{"0.txt", "1/a.txt", "1/d.txt", "1/c.txt", "1/e.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("largest") {
            pull_order = db::PullOrder::largest;
            auto expected = names_t{"0.txt", "1/e.txt", "1/c.txt", "1/d.txt", "1/a.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("oldest") {
            pull_order = db::PullOrder::oldest;
            auto expected = names_t{"0.txt", "1/c.txt", "1/d.txt", "1/a.txt", "1/e.txt"};
            CHECK(iterate() == expected);
        }
        SECTION("newest") {
            pull_order = db::PullOrder::newest;
            auto expected = names_t{"0.txt", "1/e.txt", "1/a.txt", "1/d.txt", "1/c.txt"};
            CHECK(iterate() == expected);
        }
    }
    SECTION("change iteration order") {
        auto &pull_order = ((model::folder_data_t *)folder.get())->access<test::to::pull_order>();
        pull_order = db::PullOrder::alphabetic;
        auto file_iterator = peer_device->create_iterator(*cluster);
        auto names = names_t();
        auto next = [&]() {
            auto [fi, action] = file_iterator->next();
            REQUIRE(fi);
            CHECK(action == A::remote_copy);
            names.emplace_back(std::string(fi->get_name()));
        };

        next();
        next();

        pull_order = db::PullOrder::newest;
        file_iterator->on_upsert(*folder);
        next();
        next();
        next();

        auto expected = names_t{"0.txt", "1/a.txt", "1/e.txt", "1/d.txt", "1/c.txt"};
        CHECK(names == expected);
    }
}

TEST_CASE("no file iteration for send-only folder", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();

    db::Folder db_folder;
    db::set_id(db_folder, "1234-5678");
    db::set_label(db_folder, "l");
    db::set_path(db_folder, "/my/path");
    db::set_folder_type(db_folder, db::FolderType::send);

    REQUIRE(builder.upsert_folder(db_folder).apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto folder_infos = &folder->get_folder_infos();
    REQUIRE(folder_infos->size() == 2u);

    auto file_iterator = peer_device->create_iterator(*cluster);
    auto pr_fi = proto::FileInfo();
    proto::set_name(pr_fi, "a.txt");
    proto::set_sequence(pr_fi, 10);
    REQUIRE(builder.make_index(peer_id.get_sha256(), folder->get_id()).add(pr_fi, peer_device).finish().apply());

    auto [f, action] = file_iterator->next();
    REQUIRE(!f);

    SECTION("back to send & receive") {
        db::set_folder_type(db_folder, db::FolderType::send_and_receive);
        REQUIRE(builder.upsert_folder(db_folder).apply());
        auto [f, action] = file_iterator->next();
        REQUIRE(f);
        REQUIRE(action == A::remote_copy);
        CHECK(f->get_name() == "a.txt");
    }
}

int _init() {
    test::init_logging();
    return 1;
}

static int v = _init();


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "test-utils.h"
#include "model/cluster.h"
#include "model/misc/block_iterator.h"
#include "diff-builder.h"

using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;

TEST_CASE("block iterator", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);

    auto block_iterator = block_iterator_ptr_t();
    auto next = [&](file_info_ptr_t source, bool reset = false) -> file_block_t {
        if (source && source->is_file() && !source->is_deleted()) {
            if (reset) {
                block_iterator = new blocks_iterator_t(*source);
            }
            if (block_iterator && *block_iterator) {
                return block_iterator->next();
            }
        }
        return {};
    };

    auto &folders = cluster->get_folders();
    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = folder->get_folder_infos();
    auto my_folder = folder_infos.by_device(*my_device);

    auto p_file = proto::FileInfo();
    proto::set_name(p_file, "a.txt");
    proto::set_sequence(p_file, 2);

    SECTION("no blocks") {
        REQUIRE(builder.local_update(folder->get_id(), p_file).apply());

        auto my_file = my_folder->get_file_infos().by_name(proto::get_name(p_file));
        REQUIRE(!next(my_file, true));
    }

    auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto b2_hash = utils::sha256_digest(as_bytes("567890")).value();
    auto &b1 = proto::add_blocks(p_file);
    proto::set_hash(b1, b1_hash);
    proto::set_size(b1, 5);

    SECTION("two blocks") {
        auto b1 = proto::get_blocks(p_file, 0);
        auto &b2 = proto::add_blocks(p_file);
        proto::set_hash(b2, b2_hash);
        proto::set_size(b2, 5);
        proto::set_size(p_file, 10);
        proto::set_block_size(p_file, proto::get_size(b1));

        SECTION("no iteration upon deleted file") {
            proto::set_block_size(p_file, 0);
            proto::clear_blocks(p_file);
            proto::set_deleted(p_file, true);

            REQUIRE(builder.local_update(folder->get_id(), p_file).apply());

            auto my_file = my_folder->get_file_infos().by_name(proto::get_name(p_file));
            CHECK(!next(my_file, true));
        }

        SECTION("normal iteration") {
            REQUIRE(builder.local_update(folder->get_id(), p_file).apply());

            auto my_file = my_folder->get_file_infos().by_name(proto::get_name(p_file));
            auto bi1 = cluster->get_blocks().by_hash(b1_hash);
            auto bi2 = cluster->get_blocks().by_hash(b2_hash);
            my_file->remove_blocks();
            my_file->assign_block(bi1, 0);
            my_file->assign_block(bi2, 1);

            auto fb1 = next(my_file, true);
            REQUIRE(fb1);
            CHECK(fb1.block()->get_hash() == b1_hash);
            CHECK(fb1.block_index() == 0);
            CHECK(fb1.file() == my_file.get());

            auto fb2 = next(my_file);
            REQUIRE(fb2);
            CHECK(fb2.block()->get_hash() == b2_hash);
            CHECK(fb2.block_index() == 1);
            CHECK(fb2.file() == my_file.get());

            REQUIRE(!next(my_file));
        }

        SECTION("no iteration upon unavailable file") {
            REQUIRE(builder.local_update(folder->get_id(), p_file).apply());

            auto my_file = my_folder->get_file_infos().by_name(proto::get_name(p_file));
            auto bi1 = cluster->get_blocks().by_hash(b1_hash);
            auto bi2 = cluster->get_blocks().by_hash(b2_hash);
            my_file->remove_blocks();
            my_file->assign_block(bi1, 0);
            my_file->assign_block(bi2, 1);

            my_file->mark_unreachable(true);
            CHECK(!next(my_file, true));
        }
    }

    SECTION("block is available on some other local file") {
        auto p_file_2 = proto::FileInfo();
        proto::set_name(p_file_2, "b.txt");
        proto::set_sequence(p_file_2, 2);
        proto::set_size(p_file_2, proto::get_size(b1));
        proto::set_block_size(p_file_2, proto::get_size(b1));
        proto::add_blocks(p_file_2, b1);

        proto::set_size(p_file, proto::get_size(b1));

        REQUIRE(builder.local_update(folder->get_id(), p_file_2).apply());

        auto my_file_2 = my_folder->get_file_infos().by_name(proto::get_name(p_file_2));
        REQUIRE(my_file_2->is_locally_available());

        REQUIRE(builder.local_update(folder->get_id(), p_file).apply());

        auto my_file = my_folder->get_file_infos().by_name(proto::get_name(p_file));
        auto bi1 = cluster->get_blocks().by_hash(b1_hash);
        my_file->remove_blocks();
        my_file->assign_block(bi1, 0);
        REQUIRE(!my_file->is_locally_available());

        auto fb1 = next(my_file, true);
        REQUIRE(fb1);
        CHECK(fb1.block()->get_hash() == b1_hash);
        CHECK(fb1.block_index() == 0);
        CHECK(fb1.file() == my_file.get());
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "test-utils.h"
#include "model/cluster.h"
#include "diff-builder.h"

using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;

TEST_CASE("file_info_t::local_file", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto &folders = cluster->get_folders();
    auto builder = diff_builder_t(*cluster);
    REQUIRE(builder.upsert_folder("1234-5678", "some/path", "my-label").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());

    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = folder->get_folder_infos();
    auto folder_my = folder_infos.by_device(*my_device);
    auto folder_peer = folder_infos.by_device(*peer_device);

    auto pr_file = proto::FileInfo();
    proto::set_name(pr_file, "a.txt");

    auto &v = proto::get_version(pr_file);
    proto::add_counters(v, proto::Counter(1, 1));

    SECTION("no local file") {
        auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
        CHECK(!file_peer->local_file());
    }

    SECTION("there is identical local file") {
        proto::set_sequence(pr_file, folder_my->get_max_sequence() + 1);
        auto file_my = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file_my));

        proto::set_sequence(pr_file, folder_peer->get_max_sequence() + 1);
        auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
        REQUIRE(folder_peer->add_strict(file_peer));

        auto lf = file_peer->local_file();
        REQUIRE(lf);
        REQUIRE(lf == file_my);
    }
}

TEST_CASE("file_info_t::check_consistency", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);

    auto &folders = cluster->get_folders();
    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = folder->get_folder_infos();
    auto folder_my = folder_infos.by_device(*my_device);

    auto pr_file = proto::FileInfo();
    proto::set_name(pr_file, "a.txt");
    proto::set_block_size(pr_file, 5);
    proto::set_size(pr_file, 5);

    auto b_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto &b = proto::add_blocks(pr_file);
    proto::set_hash(b, b_hash);
    proto::set_size(b, 5);

    auto bi = block_info_t::create(b).value();
    auto &blocks_map = cluster->get_blocks();
    blocks_map.put(bi);
    auto bbb = blocks_map.by_hash(b_hash);
    REQUIRE(bbb);

    REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());
    auto file_my = folder_my->get_file_infos().by_name(proto::get_name(pr_file));
    CHECK(file_my->get_size() == 5);
}

TEST_CASE("file_info_t::create, inconsistent source") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);

    auto &folders = cluster->get_folders();
    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = folders.by_id("1234-5678");
    auto &folder_infos = folder->get_folder_infos();
    auto folder_my = folder_infos.by_device(*my_device);

    auto pr_block = proto::BlockInfo();
    proto::set_size(pr_block, 131072);

    auto pr_file = proto::FileInfo();
    proto::set_name(pr_file, "a.txt");
    proto::set_block_size(pr_file, 131072);
    proto::set_size(pr_file, 0);

    auto &v = proto::get_version(pr_file);
    proto::add_counters(v, proto::Counter(my_device->device_id().get_uint(), 1));
    proto::add_blocks(pr_file, pr_block);

    auto my_file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
    CHECK(my_file->get_block_size() == 0);
    CHECK(my_file->get_blocks().size() == 0);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "fs/scan_task.h"
#include "fs/utils.h"
#include "model/misc/sequencer.h"

using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::utils;
using namespace syncspirit::model;
using namespace syncspirit::fs;

TEST_CASE("scan_task", "[fs]") {
    test::init_logging();

    auto root_path = unique_path();
    bfs::create_directories(root_path);
    path_guard_t path_quard{root_path};

    config::fs_config_t config{3600, 10, 1024 * 1024, 100};
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto peer2_id = device_id_t::from_string("XBOWTOU-Y7H6RM6-D7WT3UB-7P2DZ5G-R6GNZG6-T5CCG54-SGVF3U5-LBM7RQB").value();
    auto peer2_device = device_t::create(peer2_id, "peer2-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);
    cluster->get_devices().put(peer2_device);

    auto db_folder = db::Folder();
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "zzz");
    db::set_path(db_folder, root_path.string());

    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();
    cluster->get_folders().put(folder);

    db::FolderInfo db_folder_info;
    db::set_index_id(db_folder_info, 1234);
    db::set_max_sequence(db_folder_info, 3);

    auto folder_my = folder_info_t::create(sequencer->next_uuid(), db_folder_info, my_device, folder).value();
    auto folder_peer = folder_info_t::create(sequencer->next_uuid(), db_folder_info, peer_device, folder).value();
    auto folder_peer2 = folder_info_t::create(sequencer->next_uuid(), db_folder_info, peer2_device, folder).value();
    folder->get_folder_infos().put(folder_my);
    folder->get_folder_infos().put(folder_peer);
    folder->get_folder_infos().put(folder_peer2);

    SECTION("without files"){

#ifndef SYNCSPIRIT_WIN
        SECTION("no permissions to read dir => err"){bfs::permissions(root_path, bfs::perms::none);
    auto folder_info = folder_info_t::create(sequencer->next_uuid(), db_folder_info, my_device, folder).value();
    folder->get_folder_infos().put(folder_info);

    auto task = scan_task_t(cluster, folder->get_id(), config);
    auto r = task.advance();
    CHECK(std::get_if<scan_errors_t>(&r));

    auto errs = std::get_if<scan_errors_t>(&r);
    REQUIRE(errs->size() == 1);

    auto &err = errs->at(0);
    CHECK(err.ec);
    CHECK(err.path == root_path);

    auto &seen = task.get_seen_paths();
    REQUIRE(seen.size() == 1);
    CHECK(seen.begin()->first == "");
    CHECK(seen.begin()->second == root_path);
}
#endif

SECTION("no dirs, no files") {
    auto task = scan_task_t(cluster, folder->get_id(), config);
    auto r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == true);

    r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == false);
}

SECTION("some dirs, no files") {
    auto task = scan_task_t(cluster, folder->get_id(), config);
    auto dir = root_path / "some-dir";
    bfs::create_directories(dir);
    auto modified = to_unix(bfs::last_write_time(dir));

    auto r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == true);

    r = task.advance();
    auto *uf = std::get_if<unknown_file_t>(&r);
    REQUIRE(uf);
    CHECK(uf->path.filename() == "some-dir");
    CHECK(proto::get_size(uf->metadata) == 0);
    CHECK(proto::get_type(uf->metadata) == proto::FileInfoType::DIRECTORY);
    CHECK(proto::get_modified_s(uf->metadata) == modified);

    auto status = bfs::status(dir);
    auto perms = static_cast<uint32_t>(status.permissions());
    CHECK(proto::get_permissions(uf->metadata) == perms);

    r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == true);

    r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == false);

    auto &seen = task.get_seen_paths();
    CHECK(seen.count("some-dir"));
}

SECTION("no dirs, unknown files") {
    auto task = scan_task_t(cluster, folder->get_id(), config);
    write_file(root_path / "some-file", "");
    auto r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == true);

    r = task.advance();
    auto *uf = std::get_if<unknown_file_t>(&r);
    REQUIRE(uf);
    CHECK(uf->path.filename() == "some-file");
    CHECK(proto::get_size(uf->metadata) == 0);
    CHECK(proto::get_type(uf->metadata) == proto::FileInfoType::FILE);

    r = task.advance();
    REQUIRE(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == false);

    auto &seen = task.get_seen_paths();
    CHECK(seen.count("some-file"));
}

#ifndef SYNCSPIRIT_WIN
SECTION("no dirs, symlink to non-existing target") {
    auto task = scan_task_t(cluster, folder->get_id(), config);
    auto file_path = root_path / "symlink";
    bfs::create_symlink(bfs::path("/some/where"), file_path);

    auto r = task.advance();
    CHECK(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == true);

    r = task.advance();
    auto *uf = std::get_if<unknown_file_t>(&r);
    REQUIRE(uf);
    CHECK(uf->path.filename() == "symlink");
    CHECK(proto::get_size(uf->metadata) == 0);
    CHECK(proto::get_type(uf->metadata) == proto::FileInfoType::SYMLINK);
    CHECK(proto::get_symlink_target(uf->metadata) == "/some/where");

    r = task.advance();
    REQUIRE(std::get_if<bool>(&r));
    CHECK(*std::get_if<bool>(&r) == false);

    auto &seen = task.get_seen_paths();
    CHECK(seen.count("symlink"));
}
#endif
}

SECTION("regular files") {
    auto modified = std::int64_t{1642007468};
    auto pr_file = proto::FileInfo{};
    proto::set_name(pr_file, "a.txt");
    proto::set_sequence(pr_file, 4);

    auto &v = proto::get_version(pr_file);
    auto &counter = proto::add_counters(v);
    proto::set_id(counter, peer_device->device_id().get_uint());
    proto::set_id(counter, 1);

    SECTION("meta is not changed (file)") {
        auto path = root_path / "a.txt";
        write_file(path, "12345");
        bfs::last_write_time(path, from_unix(modified));
        auto status = bfs::status(path);
        auto perms = static_cast<uint32_t>(status.permissions());

        proto::set_block_size(pr_file, 5);
        proto::set_size(pr_file, 5);
        proto::set_modified_s(pr_file, modified);
        proto::set_permissions(pr_file, perms);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a.txt"));
    }

    SECTION("meta is not changed (dir)") {
        proto::set_name(pr_file, "a-dir");
        proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);

        auto dir = root_path / "a-dir";
        bfs::create_directories(dir);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a-dir"));
    }

    SECTION("meta is not changed (dir + file inside)") {
        auto pr_dir = pr_file;
        proto::set_name(pr_dir, "a-dir-2");
        proto::set_type(pr_dir, proto::FileInfoType::DIRECTORY);
        proto::set_sequence(pr_dir, proto::get_sequence(pr_file) + 1);

        auto dir = root_path / "a-dir-2";
        bfs::create_directories(dir);

        auto path = root_path / "a-dir-2" / "a.txt";
        write_file(path, "12345");
        bfs::last_write_time(path, from_unix(modified));
        auto status = bfs::status(path);
        auto perms = static_cast<uint32_t>(status.permissions());

        proto::set_name(pr_file, "a-dir-2/a.txt");
        proto::set_block_size(pr_file, 5);
        proto::set_size(pr_file, 5);
        proto::set_modified_s(pr_file, modified);
        proto::set_permissions(pr_file, perms);

        auto info_file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(info_file));

        auto info_dir = file_info_t::create(sequencer->next_uuid(), pr_dir, folder_my).value();
        REQUIRE(folder_my->add_strict(info_dir));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == info_dir);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == info_file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a-dir-2"));
        CHECK(seen.count("a-dir-2/a.txt"));
    }

    SECTION("file has been removed") {
        proto::set_block_size(pr_file, 5);
        proto::set_size(pr_file, 5);
        proto::set_modified_s(pr_file, modified);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<removed_t>(&r));
        auto ref = std::get_if<removed_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);
        auto &seen = task.get_seen_paths();
        CHECK(seen.count(file->get_name()));
    }

    SECTION("dir has been removed") {
        proto::set_name(pr_file, "a-dir");
        proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<removed_t>(&r));
        auto ref = std::get_if<removed_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a-dir"));
    }

    SECTION("removed file does not exist => unchanged meta") {
        proto::set_deleted(pr_file, true);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count(file->get_name()));
    }

    SECTION("removed dir does not exist => unchanged meta") {
        proto::set_deleted(pr_file, true);
        proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count(file->get_name()));
    }

    SECTION("root dir does not exist & deleted file => unchanged meta") {
        proto::set_deleted(pr_file, true);
        folder->set_path(root_path / "zzz");

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        REQUIRE(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count(file->get_name()));
    }

    SECTION("meta is changed") {
        auto task = scan_task_ptr_t{};
        auto file = file_info_ptr_t{};

        auto r = scan_result_t{};

        SECTION("file size differs") {
            auto path = root_path / "a.txt";
            write_file(path, "12345");
            bfs::last_write_time(path, from_unix(modified));
            auto status = bfs::status(path);
            auto perms = static_cast<uint32_t>(status.permissions());

            proto::set_block_size(pr_file, 5);
            proto::set_size(pr_file, 6);
            proto::set_modified_s(pr_file, modified);
            proto::set_permissions(pr_file, perms);

            file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
            REQUIRE(folder_my->add_strict(file));

            task = new scan_task_t(cluster, folder->get_id(), config);

            r = task->advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task->advance();
            REQUIRE(std::get_if<changed_meta_t>(&r));
            auto ref = std::get_if<changed_meta_t>(&r);
            CHECK(ref->file == file);
            CHECK(proto::get_size(ref->metadata) == 5);
            CHECK(proto::get_modified_s(ref->metadata) == modified);
        }

        SECTION("modification time differs") {
            auto path = root_path / "a.txt";
            write_file(path, "12345");
            bfs::last_write_time(path, from_unix(modified));
            auto status = bfs::status(path);
            auto perms = static_cast<uint32_t>(status.permissions());

            proto::set_block_size(pr_file, 5);
            proto::set_size(pr_file, 5);
            proto::set_modified_s(pr_file, modified + 1);
            proto::set_permissions(pr_file, perms);

            file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
            REQUIRE(folder_my->add_strict(file));

            task = new scan_task_t(cluster, folder->get_id(), config);

            r = task->advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task->advance();
            REQUIRE(std::get_if<changed_meta_t>(&r));
            auto ref = std::get_if<changed_meta_t>(&r);
            CHECK(ref->file == file);
            CHECK(proto::get_size(ref->metadata) == 5);
            CHECK(proto::get_modified_s(ref->metadata) == modified);
        }

        SECTION("permissions differs") {
            proto::set_block_size(pr_file, 5);
            proto::set_size(pr_file, 5);
            proto::set_modified_s(pr_file, modified);
            proto::set_permissions(pr_file, static_cast<uint32_t>(-1));

            auto path = root_path / "a.txt";
            write_file(path, "12345");
            bfs::last_write_time(path, from_unix(modified));

            file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
            REQUIRE(folder_my->add_strict(file));

            task = new scan_task_t(cluster, folder->get_id(), config);

            r = task->advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task->advance();
            REQUIRE(std::get_if<changed_meta_t>(&r));
            auto ref = std::get_if<changed_meta_t>(&r);
            CHECK(ref->file == file);
        }

        r = task->advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task->get_seen_paths();
        CHECK(seen.count(file->get_name()));
    }

    SECTION("tmp") {
        proto::set_block_size(pr_file, 5);
        proto::set_size(pr_file, 5);
        proto::set_modified_s(pr_file, modified);

        auto path = root_path / "a.txt.syncspirit-tmp";

        SECTION("size match -> ok, will recalc") {
            write_file(path, "12345");

            auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
            REQUIRE(folder_peer->add_strict(file_peer));

            auto task = scan_task_t(cluster, folder->get_id(), config);
            auto r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task.advance();
            REQUIRE(std::get_if<incomplete_t>(&r));
            auto ref = std::get_if<incomplete_t>(&r);
            CHECK(ref->file);
            CHECK(ref->opened_file);

            r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == false);

            auto &seen = task.get_seen_paths();
            CHECK(seen.count("a.txt"));
        }

        SECTION("size mismatch -> remove & ignore") {
            write_file(path, "123456");

            auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
            REQUIRE(folder_peer->add_strict(file_peer));

            auto task = scan_task_t(cluster, folder->get_id(), config);
            auto r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task.advance();
            CHECK(std::get_if<incomplete_removed_t>(&r));
            CHECK(std::get_if<incomplete_removed_t>(&r)->file == file_peer);

            r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == false);
            CHECK(!bfs::exists(path));

            auto &seen = task.get_seen_paths();
            CHECK(seen.count("a.txt"));
        }

        SECTION("size mismatch for global source -> remove & ignore") {
            write_file(path, "123456");

            auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
            REQUIRE(folder_peer->add_strict(file_peer));

            proto::set_size(pr_file, file_peer->get_size() + 10);
            proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 2));

            auto file_peer2 = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer2).value();
            REQUIRE(folder_peer2->add_strict(file_peer2));

            auto task = scan_task_t(cluster, folder->get_id(), config);
            auto r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task.advance();
            CHECK(std::get_if<incomplete_removed_t>(&r));
            CHECK(std::get_if<incomplete_removed_t>(&r)->file == file_peer2);

            r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == false);
            CHECK(!bfs::exists(path));

            auto &seen = task.get_seen_paths();
            CHECK(seen.count("a.txt"));
        }

        SECTION("no source -> remove") {
            write_file(path, "123456");

            auto task = scan_task_t(cluster, folder->get_id(), config);
            auto r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task.advance();
            CHECK(std::get_if<orphaned_removed_t>(&r));
            CHECK(std::get_if<orphaned_removed_t>(&r)->path == path);

            r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == false);
            CHECK(!bfs::exists(path));

            auto &seen = task.get_seen_paths();
            CHECK(!seen.count("a.txt"));
        }
    }
    SECTION("tmp & non-tmp: both are returned") {

        auto path = root_path / "a.txt";
        auto path_tmp = root_path / "a.txt.syncspirit-tmp";
        write_file(path, "12345");
        write_file(path_tmp, "12345");
        bfs::last_write_time(path, from_unix(modified));
        auto status = bfs::status(path);
        auto perms = static_cast<uint32_t>(status.permissions());

        proto::set_block_size(pr_file, 5);
        proto::set_size(pr_file, 5);
        proto::set_modified_s(pr_file, modified);
        proto::set_permissions(pr_file, perms);
        auto file_my = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();

        proto::set_id(counter, 10);
        auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
        REQUIRE(folder_my->add_strict(file_my));
        REQUIRE(folder_peer->add_strict(file_peer));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        unchanged_meta_t unchanged;
        incomplete_t incomplete;
        for (int i = 0; i < 2; ++i) {
            r = task.advance();
            std::visit(
                [&](auto &&it) {
                    using T = std::decay_t<decltype(it)>;
                    if constexpr (std::is_same_v<T, unchanged_meta_t>) {
                        unchanged = it;
                    } else if constexpr (std::is_same_v<T, incomplete_t>) {
                        incomplete = it;
                    } else {
                        REQUIRE((0 && "unexpected result"));
                    }
                },
                r);
        }
        CHECK(unchanged.file == file_my);
        CHECK(incomplete.file);
        CHECK(incomplete.opened_file);
        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count(path.filename().string()));
        CHECK(seen.count(path_tmp.filename().string()));
    }

    SECTION("cannot read file error") {
        proto::set_name(pr_file, "a.txt");
        auto path = root_path / "a.txt";
        write_file(path, "12345");

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        bfs::remove(path);
        r = task.advance();
        REQUIRE(std::get_if<file_error_t>(&r));
        auto err = std::get_if<file_error_t>(&r);
        REQUIRE(err->path == path);
        REQUIRE(err->ec);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count(path.filename().string()));
    }

    SECTION("cannot read dir, error") {
        proto::set_name(pr_file, "some/a.txt");
        auto path = root_path / "some" / "a.txt";
        auto parent = path.parent_path();
        write_file(path, "12345");
        sys::error_code ec;
        bfs::permissions(parent, bfs::perms::none, ec);
        bfs::permissions(path, bfs::perms::owner_read, ec);
        if (ec) {
            auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
            REQUIRE(folder_my->add_strict(file));

            auto task = scan_task_t(cluster, folder->get_id(), config);
            auto r = task.advance();
            CHECK(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == true);

            r = task.advance();
            auto *uf = std::get_if<unknown_file_t>(&r);
            REQUIRE(uf);
            CHECK(uf->path.filename() == "some");
            CHECK(proto::get_size(uf->metadata) == 0);
            CHECK(proto::get_type(uf->metadata) == proto::FileInfoType::DIRECTORY);

            r = task.advance();
            REQUIRE(std::get_if<scan_errors_t>(&r));
            auto errs = std::get_if<scan_errors_t>(&r);
            REQUIRE(errs);
            REQUIRE(errs->size() == 1);
            REQUIRE(errs->at(0).path == parent);
            REQUIRE(errs->at(0).ec);

            r = task.advance();
            REQUIRE(std::get_if<bool>(&r));
            CHECK(*std::get_if<bool>(&r) == false);
            bfs::permissions(parent, bfs::perms::all);

            auto &seen = task.get_seen_paths();
            CHECK(seen.count("some"));
            CHECK(seen.count("some/a.txt"));
        }
    }
}

#ifndef SYNCSPIRIT_WIN
SECTION("symlink file") {
    auto modified = std::time_t{1642007468};
    auto pr_file = proto::FileInfo{};

    proto::set_name(pr_file, "a.txt");
    proto::set_sequence(pr_file, 4);
    proto::set_type(pr_file, proto::FileInfoType::SYMLINK);
    proto::set_symlink_target(pr_file, "b.txt");

    auto &v = proto::get_version(pr_file);
    proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 1));

    SECTION("symlink does not exists") {
        proto::set_modified_s(pr_file, modified);

        auto path = root_path / "a.txt";
        auto target = bfs::path("b.txt");
        bfs::create_symlink(target, path);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a.txt"));
    }

    SECTION("symlink does exists") {
        proto::set_modified_s(pr_file, modified);
        proto::set_symlink_target(pr_file, "b");

        auto path = root_path / "a.txt";
        auto target = bfs::path("b");
        bfs::create_symlink(target, path);
        bfs::create_directories(root_path / target);

        auto pr_dir = proto::FileInfo{};
        proto::set_name(pr_dir, "b");
        proto::set_sequence(pr_dir, proto::get_sequence(pr_file) + 1);
        proto::set_type(pr_dir, proto::FileInfoType::DIRECTORY);
        proto::set_version(pr_dir, v);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        auto dir = file_info_t::create(sequencer->next_uuid(), pr_dir, folder_my).value();
        REQUIRE(folder_my->add_strict(file));
        REQUIRE(folder_my->add_strict(dir));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(((ref->file == dir) || (ref->file == file)));

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(((ref->file == dir) || (ref->file == file)));

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a.txt"));
    }

    SECTION("symlink to root") {
        proto::set_modified_s(pr_file, modified);
        proto::set_symlink_target(pr_file, "/");

        auto path = root_path / "a.txt";
        auto target = bfs::path("/");
        bfs::create_symlink(target, path);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<unchanged_meta_t>(&r));
        auto ref = std::get_if<unchanged_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a.txt"));
    }

    SECTION("symlink points to something different") {
        proto::set_modified_s(pr_file, modified);

        auto path = root_path / "a.txt";
        auto target = bfs::path("c.txt");
        bfs::create_symlink(target, path);

        auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
        REQUIRE(folder_my->add_strict(file));

        auto task = scan_task_t(cluster, folder->get_id(), config);
        auto r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == true);

        r = task.advance();
        REQUIRE(std::get_if<changed_meta_t>(&r));
        auto ref = std::get_if<changed_meta_t>(&r);
        CHECK(ref->file == file);

        r = task.advance();
        CHECK(std::get_if<bool>(&r));
        CHECK(*std::get_if<bool>(&r) == false);

        auto &seen = task.get_seen_paths();
        CHECK(seen.count("a.txt"));
    }
}
#endif
}