返回 douyin-downloader
test_database.py
根目录 / tests / test_database.py
1 import asyncio
2 import json
3
4 import pytest
5
6 from storage import Database
7
8
9 @pytest.mark.asyncio
10 async def test_database_aweme_lifecycle(tmp_path):
11 db_path = tmp_path / "test.db"
12 database = Database(str(db_path))
13
14 await database.initialize()
15
16 aweme_payload = {
17 "aweme_id": "123",
18 "aweme_type": "video",
19 "title": "test",
20 "author_id": "author",
21 "author_name": "Author",
22 "create_time": 1700000000,
23 "file_path": "/tmp",
24 "metadata": json.dumps({"a": 1}, ensure_ascii=False),
25 }
26
27 await database.add_aweme(aweme_payload)
28
29 assert await database.is_downloaded("123") is True
30 assert await database.get_aweme_count_by_author("author") == 1
31 assert await database.get_latest_aweme_time("author") == 1700000000
32
33 await database.add_history(
34 {
35 "url": "https://www.douyin.com/video/123",
36 "url_type": "video",
37 "total_count": 1,
38 "success_count": 1,
39 "config": json.dumps({"path": "./Downloaded/"}, ensure_ascii=False),
40 }
41 )
42
43 await database.close()
44
45
46 @pytest.mark.asyncio
47 async def test_database_transcript_job_upsert(tmp_path):
48 db_path = tmp_path / "test.db"
49 database = Database(str(db_path))
50 await database.initialize()
51
52 await database.upsert_transcript_job(
53 {
54 "aweme_id": "123",
55 "video_path": "/tmp/demo.mp4",
56 "transcript_dir": "/tmp",
57 "text_path": "/tmp/demo.transcript.txt",
58 "json_path": "/tmp/demo.transcript.json",
59 "model": "gpt-4o-mini-transcribe",
60 "status": "skipped",
61 "skip_reason": "missing_api_key",
62 "error_message": None,
63 }
64 )
65
66 row = await database.get_transcript_job("123")
67 assert row is not None
68 assert row["status"] == "skipped"
69 assert row["skip_reason"] == "missing_api_key"
70
71 await database.upsert_transcript_job(
72 {
73 "aweme_id": "123",
74 "video_path": "/tmp/demo.mp4",
75 "transcript_dir": "/tmp",
76 "text_path": "/tmp/demo.transcript.txt",
77 "json_path": "/tmp/demo.transcript.json",
78 "model": "gpt-4o-mini-transcribe",
79 "status": "success",
80 "skip_reason": None,
81 "error_message": None,
82 }
83 )
84
85 row = await database.get_transcript_job("123")
86 assert row["status"] == "success"
87 assert row["skip_reason"] is None
88
89 await database.close()
90
91
92 @pytest.mark.asyncio
93 async def test_database_initialize_sets_wal_journal_mode(tmp_path):
94 db_path = tmp_path / "test.db"
95 database = Database(str(db_path))
96 await database.initialize()
97
98 db = await database._get_conn()
99 cursor = await db.execute("PRAGMA journal_mode")
100 row = await cursor.fetchone()
101 assert row is not None
102 assert str(row[0]).lower() == "wal"
103
104 cursor = await db.execute("PRAGMA synchronous")
105 row = await cursor.fetchone()
106 # synchronous=NORMAL == 1
107 assert row is not None
108 assert int(row[0]) == 1
109
110 await database.close()
111
112
113 @pytest.mark.asyncio
114 async def test_add_aweme_batch_inserts_all_items(tmp_path):
115 db_path = tmp_path / "test.db"
116 database = Database(str(db_path))
117 await database.initialize()
118
119 items = [
120 {
121 "aweme_id": str(i),
122 "aweme_type": "video",
123 "title": f"title-{i}",
124 "author_id": "author",
125 "author_name": "Author",
126 "create_time": 1700000000 + i,
127 "file_path": "/tmp",
128 "metadata": json.dumps({"i": i}, ensure_ascii=False),
129 }
130 for i in range(100)
131 ]
132
133 await database.add_aweme_batch(items)
134
135 assert await database.get_aweme_count_by_author("author") == 100
136 for i in range(100):
137 assert await database.is_downloaded(str(i)) is True
138
139 await database.close()
140
141
142 @pytest.mark.asyncio
143 async def test_add_aweme_batch_empty_list_is_noop(tmp_path):
144 db_path = tmp_path / "test.db"
145 database = Database(str(db_path))
146 await database.initialize()
147
148 await database.add_aweme_batch([])
149
150 assert await database.get_aweme_count_by_author("author") == 0
151
152 await database.close()
153
154
155 @pytest.mark.asyncio
156 async def test_add_aweme_batch_replaces_on_conflict(tmp_path):
157 db_path = tmp_path / "test.db"
158 database = Database(str(db_path))
159 await database.initialize()
160
161 base = {
162 "aweme_id": "777",
163 "aweme_type": "video",
164 "title": "first",
165 "author_id": "author",
166 "author_name": "Author",
167 "create_time": 1700000000,
168 "file_path": "/tmp/a",
169 "metadata": json.dumps({"v": 1}, ensure_ascii=False),
170 }
171 await database.add_aweme_batch([base])
172
173 updated = dict(base)
174 updated["title"] = "second"
175 updated["file_path"] = "/tmp/b"
176 await database.add_aweme_batch([updated])
177
178 db = await database._get_conn()
179 cursor = await db.execute("SELECT title, file_path FROM aweme WHERE aweme_id = ?", ("777",))
180 row = await cursor.fetchone()
181 assert row == ("second", "/tmp/b")
182
183 cursor = await db.execute("SELECT COUNT(*) FROM aweme WHERE aweme_id = ?", ("777",))
184 count_row = await cursor.fetchone()
185 assert count_row[0] == 1
186
187 await database.close()
188
189
190 @pytest.mark.asyncio
191 async def test_add_aweme_batch_uses_single_commit(tmp_path, monkeypatch):
192 db_path = tmp_path / "test.db"
193 database = Database(str(db_path))
194 await database.initialize()
195
196 db = await database._get_conn()
197 commit_count = {"n": 0}
198 original_commit = db.commit
199
200 async def counting_commit():
201 commit_count["n"] += 1
202 return await original_commit()
203
204 monkeypatch.setattr(db, "commit", counting_commit)
205
206 items = [
207 {
208 "aweme_id": str(i),
209 "aweme_type": "video",
210 "title": f"t{i}",
211 "author_id": "a",
212 "author_name": "A",
213 "create_time": 1700000000 + i,
214 "file_path": "/tmp",
215 "metadata": "{}",
216 }
217 for i in range(50)
218 ]
219 await database.add_aweme_batch(items)
220
221 assert commit_count["n"] == 1, (
222 f"expected exactly 1 commit for batch insert, got {commit_count['n']}"
223 )
224
225 await database.close()
226
227
228 @pytest.mark.asyncio
229 async def test_database_get_conn_reuses_single_connection_under_concurrency(tmp_path, monkeypatch):
230 import storage.database as database_module
231
232 connect_calls = []
233
234 class _FakeConn:
235 def __init__(self, db_path: str):
236 self.db_path = db_path
237 self.closed = False
238
239 async def close(self):
240 self.closed = True
241
242 async def _fake_connect(db_path: str):
243 connect_calls.append(db_path)
244 await asyncio.sleep(0)
245 return _FakeConn(db_path)
246
247 monkeypatch.setattr(database_module.aiosqlite, "connect", _fake_connect)
248
249 database = Database(str(tmp_path / "test.db"))
250 conn_a, conn_b = await asyncio.gather(database._get_conn(), database._get_conn())
251
252 assert conn_a is conn_b
253 assert connect_calls == [str(tmp_path / "test.db")]
254
255 await database.close()
256
256 lines PYTHON