#!/usr/bin/env python3 from __future__ import annotations import importlib.util import json import pathlib import sqlite3 import tempfile import unittest MODULE_PATH = pathlib.Path(__file__).with_name("hlcc-bootstrap.py") SPEC = importlib.util.spec_from_file_location("hlcc_bootstrap", MODULE_PATH) assert SPEC and SPEC.loader MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) SCHEMA = """ create table user ( id integer primary key autoincrement, lower_name text not null, name text not null, email text not null, passwd text not null, salt text, passwd_hash_algo text, avatar text not null, avatar_email text not null, type integer default 0, is_active integer default 1, is_admin integer default 0, num_repos integer default 0, num_stars integer default 0, num_followers integer default 0, num_following integer default 0, use_custom_avatar integer default 0, prohibit_login integer default 0 ); create table repository ( id integer primary key autoincrement, owner_id integer not null, name text not null ); create table access_token ( id integer primary key autoincrement, uid integer not null, name text not null, token_hash text not null ); """ class OwnerMigrationTests(unittest.TestCase): def setUp(self) -> None: self.temporary = tempfile.TemporaryDirectory() root = pathlib.Path(self.temporary.name) self.old = root / "old.db" self.new = root / "new.db" self.receipt = root / "receipt.json" for database in (self.old, self.new): connection = sqlite3.connect(database) connection.executescript(SCHEMA) connection.commit() connection.close() connection = sqlite3.connect(self.old) connection.execute( """ insert into user ( lower_name, name, email, passwd, salt, passwd_hash_algo, avatar, avatar_email, is_active, is_admin, num_repos, num_stars, num_followers, num_following, use_custom_avatar ) values (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 12, 4, 3, 2, 1) """, ( "bingshuo", "bingshuo", "owner@example.invalid", "preserved-password-hash", "preserved-salt", "pbkdf2$50000$50", "legacy-avatar", "avatar@example.invalid", ), ) connection.execute( "insert into repository (owner_id, name) values (1, 'legacy-repo')" ) connection.execute( "insert into access_token (uid, name, token_hash) values (1, 'legacy-token', 'secret-hash')" ) connection.commit() connection.close() def tearDown(self) -> None: self.temporary.cleanup() def test_migrates_only_owner_identity_and_preserves_password_hash(self) -> None: result = MODULE.migrate_owner_identity(self.old, self.new, self.receipt) self.assertEqual(result, "migrated") connection = sqlite3.connect(self.new) owner = connection.execute( """ select lower_name, passwd, salt, passwd_hash_algo, is_active, is_admin, num_repos, num_stars, num_followers, num_following, use_custom_avatar from user """ ).fetchone() self.assertEqual( owner, ( "bingshuo", "preserved-password-hash", "preserved-salt", "pbkdf2$50000$50", 1, 1, 0, 0, 0, 0, 0, ), ) self.assertEqual(connection.execute("select count(*) from repository").fetchone()[0], 0) self.assertEqual(connection.execute("select count(*) from access_token").fetchone()[0], 0) connection.close() receipt = json.loads(self.receipt.read_text(encoding="utf-8")) self.assertTrue(receipt["identity_only"]) self.assertFalse(receipt["access_tokens_migrated"]) self.assertFalse(receipt["repositories_migrated"]) def test_is_idempotent(self) -> None: self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "migrated") self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "already-present") if __name__ == "__main__": unittest.main()