Repository: bitcoin/bitcoin
File: test/functional/wallet_migration.py (Python)

Code before:
        # Verify the /wallets/ path exists
        assert self.master_node.wallets_path.exists()
        # Check backup file exists. Because the wallet has no name, the backup is prefixed with 'default_wallet'
        backup_path = self.master_node.wallets_path / f"default_wallet_{mocked_time}.legacy.bak"
        assert backup_path.exists()
        # Verify the original unnamed wallet was restored
        assert (self.master_node.wallets_path / "wallet.dat").exists()
        # And verify it is still a BDB wallet
        self.assert_is_bdb("")

        # Test cleanup: clear default wallet for next test
        self.clear_default_wallet(backup_path)

    def test_direct_file(self):
        self.log.info("Test migration of a wallet that is not in a wallet directory")
        wallet = self.create_legacy_wallet("plainfile")
        wallet.unloadwallet()

        shutil.copyfile(
            self.old_node.wallets_path / "plainfile" / "wallet.dat" ,
            self.master_node.wallets_path / "plainfile"
        )
        assert (self.master_node.wallets_path / "plainfile").is_file()

        mocked_time = int(time.time())
        self.master_node.setmocktime(mocked_time)
        migrate_res = self.master_node.migratewallet("plainfile")
        assert_equal(f"plainfile_{mocked_time}.legacy.bak", os.path.basename(migrate_res["backup_path"]))
        wallet = self.master_node.get_wallet_rpc("plainfile")
        info = wallet.getwalletinfo()
        assert_equal(info["descriptors"], True)
        assert_equal(info["format"], "sqlite")

        assert (self.master_node.wallets_path / "plainfile").is_dir()
        assert (self.master_node.wallets_path / "plainfile" / "wallet.dat").is_file()

    def test_addressbook(self):
        df_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)

        self.log.info("Test migration of address book data")
        wallet = self.create_legacy_wallet("legacy_addrbook")
        df_wallet.sendtoaddress(wallet.getnewaddress(), 3)

        # Import watch-only script to create a watch-only wallet after migration
        watch_addr = df_wallet.getnewaddress()
        wallet.importaddress(watch_addr)
        df_wallet.sendtoaddress(watch_addr, 2)

        # Import solvable script
        multi_addr1 = wallet.getnewaddress()
        multi_addr2 = wallet.getnewaddress()

Review comment (suggestion):
https://github.com/bitcoin/bitcoin/blob/6c3fb719d1a8f34352e6518e28eda0f46ce54ce7/test/functional/wallet_migration.py#L70-L75

It took me some time to convince myself that why is this file `self.master_node.wallets_path / wallet_name / self.wallet_data_filename` even opened for absolute path wallet name because of the absolute path wallet name residing outside the `wallets_path` dir. From the logs on my machine:

```zsh
assert_is_bdb!
self.master_node.wallets_path: /var/folders/6v/mpspb4bx4zzf3xr2z96hgq9h0000gn/T/bitcoin_func_test_yfvb1rj7/node0/regtest/wallets
wallet_name: /var/folders/6v/mpspb4bx4zzf3xr2z96hgq9h0000gn/T/bitcoin_func_test_yfvb1rj7/node0/absolute_path
self.master_node.wallets_path / wallet_name / self.wallet_data_filename:  /var/folders/6v/mpspb4bx4zzf3xr2z96hgq9h0000gn/T/bitcoin_func_test_yfvb1rj7/node0/absolute_path/wallet.dat
Within open()!
```

It's because of this caveat in `pathlib`: https://docs.python.org/3/library/pathlib.html#:~:text=If%20a%20segment%20is%20an%20absolute%20path%2C%20all%20previous%20segments%20are%20ignored

```python
If a segment is an absolute path, all previous segments are ignored (like [os.path.join()](https://docs.python.org/3/library/os.path.html#os.path.join)):

>>> PurePath('/etc', '/usr', 'lib64')
PurePosixPath('/usr/lib64')
>>> PureWindowsPath('c:/Windows', 'd:bar')
PureWindowsPath('d:bar')
```

Code after:
        # we make migration fail by placing a wallet.dat file there.
        wo_prefix = wallet_name or "default_wallet"
        # wo_prefix might have path characters in it, this corresponds with
        # DoMigration().
        wo_dirname = f"{wo_prefix}_watchonly"
        watch_only_dir = self.master_node.wallets_path / wo_dirname
        os.mkdir(watch_only_dir)
        shutil.copyfile(old_path / "wallet.dat", watch_only_dir / "wallet.dat")

        mocked_time = int(time.time())
        self.master_node.setmocktime(mocked_time)
        assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, wallet_name)
        self.master_node.setmocktime(0)

        # Verify the /wallets/ path exists.
        assert self.master_node.wallets_path.exists()

        # Verify both wallet paths exist.
        assert Path(old_path / "wallet.dat").exists()
        assert Path(master_path / "wallet.dat").exists()

        backup_prefix = "default_wallet" if is_default else os.path.basename(os.path.abspath(master_path))
        backup_path = self.master_node.wallets_path / f"{backup_prefix}_{mocked_time}.legacy.bak"
        assert backup_path.exists()

        self.assert_is_bdb(wallet_name)

        # Cleanup
        if is_default:
            self.clear_default_wallet(backup_path)
        else:
            backup_path.unlink()
            Path(watch_only_dir / "wallet.dat").unlink()
            Path(watch_only_dir).rmdir()
            Path(master_path / "wallet.dat").unlink()
            Path(old_path / "wallet.dat").unlink(missing_ok=True)

    def test_direct_file(self):
        self.log.info("Test migration of a wallet that is not in a wallet directory")
        wallet = self.create_legacy_wallet("plainfile")
        wallet.unloadwallet()

        shutil.copyfile(
            self.old_node.wallets_path / "plainfile" / "wallet.dat" ,
            self.master_node.wallets_path / "plainfile"
        )
        assert (self.master_node.wallets_path / "plainfile").is_file()

        mocked_time = int(time.time())
        self.master_node.setmocktime(mocked_time)
        migrate_res = self.master_node.migratewallet("plainfile")