|
1 | | -import uuid |
2 | | - |
3 | | -import pytest |
| 1 | +import sqlalchemy as sa |
4 | 2 | from sqlalchemy.orm import Session |
5 | 3 |
|
6 | | -from app.assets.helpers import get_utc_now |
7 | | -from app.assets.database.models import Asset |
8 | | -from app.assets.database.queries import ( |
9 | | - asset_exists_by_hash, |
10 | | - get_asset_by_hash, |
11 | | - upsert_asset, |
12 | | - bulk_insert_assets, |
13 | | - update_asset_hash_and_mime, |
14 | | -) |
15 | | - |
16 | | - |
17 | | -class TestAssetExistsByHash: |
18 | | - @pytest.mark.parametrize( |
19 | | - "setup_hash,query_hash,expected", |
20 | | - [ |
21 | | - (None, "nonexistent", False), # No asset exists |
22 | | - ("blake3:abc123", "blake3:abc123", True), # Asset exists with matching hash |
23 | | - (None, "", False), # Null hash in DB doesn't match empty string |
24 | | - ], |
25 | | - ids=["nonexistent", "existing", "null_hash_no_match"], |
26 | | - ) |
27 | | - def test_exists_by_hash(self, session: Session, setup_hash, query_hash, expected): |
28 | | - if setup_hash is not None or query_hash == "": |
29 | | - asset = Asset(hash=setup_hash, size_bytes=100) |
30 | | - session.add(asset) |
31 | | - session.commit() |
32 | | - |
33 | | - assert asset_exists_by_hash(session, asset_hash=query_hash) is expected |
34 | | - |
35 | | - |
36 | | -class TestGetAssetByHash: |
37 | | - @pytest.mark.parametrize( |
38 | | - "setup_hash,query_hash,should_find", |
39 | | - [ |
40 | | - (None, "nonexistent", False), |
41 | | - ("blake3:def456", "blake3:def456", True), |
42 | | - ], |
43 | | - ids=["nonexistent", "existing"], |
44 | | - ) |
45 | | - def test_get_by_hash(self, session: Session, setup_hash, query_hash, should_find): |
46 | | - if setup_hash is not None: |
47 | | - asset = Asset(hash=setup_hash, size_bytes=200, mime_type="image/png") |
48 | | - session.add(asset) |
49 | | - session.commit() |
50 | | - |
51 | | - result = get_asset_by_hash(session, asset_hash=query_hash) |
52 | | - if should_find: |
53 | | - assert result is not None |
54 | | - assert result.size_bytes == 200 |
55 | | - assert result.mime_type == "image/png" |
56 | | - else: |
57 | | - assert result is None |
58 | | - |
59 | | - |
60 | | -class TestUpsertAsset: |
61 | | - @pytest.mark.parametrize( |
62 | | - "first_size,first_mime,second_size,second_mime,expect_created,expect_updated,final_size,final_mime", |
63 | | - [ |
64 | | - # New asset creation |
65 | | - (None, None, 1024, "application/octet-stream", True, False, 1024, "application/octet-stream"), |
66 | | - # Existing asset, same values - no update |
67 | | - (500, "text/plain", 500, "text/plain", False, False, 500, "text/plain"), |
68 | | - # Existing asset with size 0, update with new values |
69 | | - (0, None, 2048, "image/png", False, True, 2048, "image/png"), |
70 | | - # Existing asset, second call with size 0 - no update |
71 | | - (1000, None, 0, None, False, False, 1000, None), |
72 | | - ], |
73 | | - ids=["new_asset", "existing_no_change", "update_from_zero", "zero_size_no_update"], |
74 | | - ) |
75 | | - def test_upsert_scenarios( |
76 | | - self, |
77 | | - session: Session, |
78 | | - first_size, |
79 | | - first_mime, |
80 | | - second_size, |
81 | | - second_mime, |
82 | | - expect_created, |
83 | | - expect_updated, |
84 | | - final_size, |
85 | | - final_mime, |
86 | | - ): |
87 | | - asset_hash = f"blake3:test_{first_size}_{second_size}" |
88 | | - |
89 | | - # First upsert (if first_size is not None, we're testing the second call) |
90 | | - if first_size is not None: |
91 | | - upsert_asset( |
92 | | - session, |
93 | | - asset_hash=asset_hash, |
94 | | - size_bytes=first_size, |
95 | | - mime_type=first_mime, |
96 | | - ) |
97 | | - session.commit() |
98 | | - |
99 | | - # The upsert call we're testing |
100 | | - asset, created, updated = upsert_asset( |
101 | | - session, |
102 | | - asset_hash=asset_hash, |
103 | | - size_bytes=second_size, |
104 | | - mime_type=second_mime, |
105 | | - ) |
106 | | - session.commit() |
107 | | - |
108 | | - assert created is expect_created |
109 | | - assert updated is expect_updated |
110 | | - assert asset.size_bytes == final_size |
111 | | - assert asset.mime_type == final_mime |
112 | | - |
113 | | - |
114 | | -class TestBulkInsertAssets: |
115 | | - def test_inserts_multiple_assets(self, session: Session): |
116 | | - now = get_utc_now() |
117 | | - rows = [ |
118 | | - {"id": str(uuid.uuid4()), "hash": "blake3:bulk1", "size_bytes": 100, "mime_type": "text/plain", "created_at": now}, |
119 | | - {"id": str(uuid.uuid4()), "hash": "blake3:bulk2", "size_bytes": 200, "mime_type": "image/png", "created_at": now}, |
120 | | - {"id": str(uuid.uuid4()), "hash": "blake3:bulk3", "size_bytes": 300, "mime_type": None, "created_at": now}, |
121 | | - ] |
122 | | - bulk_insert_assets(session, rows) |
123 | | - session.commit() |
124 | | - |
125 | | - assets = session.query(Asset).all() |
126 | | - assert len(assets) == 3 |
127 | | - hashes = {a.hash for a in assets} |
128 | | - assert hashes == {"blake3:bulk1", "blake3:bulk2", "blake3:bulk3"} |
129 | | - |
130 | | - def test_empty_list_is_noop(self, session: Session): |
131 | | - bulk_insert_assets(session, []) |
132 | | - session.commit() |
133 | | - assert session.query(Asset).count() == 0 |
134 | | - |
135 | | - def test_handles_large_batch(self, session: Session): |
136 | | - """Test chunking logic with more rows than MAX_BIND_PARAMS allows.""" |
137 | | - now = get_utc_now() |
138 | | - rows = [ |
139 | | - {"id": str(uuid.uuid4()), "hash": f"blake3:large{i}", "size_bytes": i, "mime_type": None, "created_at": now} |
140 | | - for i in range(200) |
141 | | - ] |
142 | | - bulk_insert_assets(session, rows) |
143 | | - session.commit() |
144 | | - |
145 | | - assert session.query(Asset).count() == 200 |
| 4 | +from app.assets.database.models import AssetContent |
| 5 | +from app.assets.database.queries import create_content, create_record |
146 | 6 |
|
147 | 7 |
|
148 | | -class TestMimeTypeImmutability: |
149 | | - """mime_type on Asset is write-once: set on first ingest, never overwritten.""" |
| 8 | +def test_hash_is_a_content_fact(session: Session) -> None: |
| 9 | + content = create_content(session, "/models/a.safetensors", hash="same", size_bytes=12) |
| 10 | + record = create_record(session, content.id, "a.safetensors") |
150 | 11 |
|
151 | | - @pytest.mark.parametrize( |
152 | | - "initial_mime,second_mime,expected_mime", |
153 | | - [ |
154 | | - ("image/png", "image/jpeg", "image/png"), |
155 | | - (None, "image/png", "image/png"), |
156 | | - ], |
157 | | - ids=["preserves_existing", "fills_null"], |
158 | | - ) |
159 | | - def test_upsert_mime_immutability(self, session: Session, initial_mime, second_mime, expected_mime): |
160 | | - h = f"blake3:upsert_{initial_mime}_{second_mime}" |
161 | | - upsert_asset(session, asset_hash=h, size_bytes=100, mime_type=initial_mime) |
162 | | - session.commit() |
| 12 | + assert session.scalar(sa.select(AssetContent).where(AssetContent.hash == "same")) == content |
| 13 | + assert record.content_id == content.id |
163 | 14 |
|
164 | | - asset, created, _ = upsert_asset(session, asset_hash=h, size_bytes=100, mime_type=second_mime) |
165 | | - assert created is False |
166 | | - assert asset.mime_type == expected_mime |
167 | 15 |
|
168 | | - @pytest.mark.parametrize( |
169 | | - "initial_mime,update_mime,update_hash,expected_mime,expected_hash", |
170 | | - [ |
171 | | - (None, "image/png", None, "image/png", "blake3:upd0"), |
172 | | - ("image/png", "image/jpeg", None, "image/png", "blake3:upd1"), |
173 | | - ("image/png", "image/jpeg", "blake3:upd2_new", "image/png", "blake3:upd2_new"), |
174 | | - ], |
175 | | - ids=["fills_null", "preserves_existing", "hash_updates_mime_locked"], |
176 | | - ) |
177 | | - def test_update_asset_hash_and_mime_immutability( |
178 | | - self, session: Session, initial_mime, update_mime, update_hash, expected_mime, expected_hash, |
179 | | - ): |
180 | | - h = expected_hash.removesuffix("_new") |
181 | | - asset = Asset(hash=h, size_bytes=100, mime_type=initial_mime) |
182 | | - session.add(asset) |
183 | | - session.flush() |
| 16 | +def test_equal_hashes_do_not_merge_content_rows(session: Session) -> None: |
| 17 | + first = create_content(session, "/models/a.safetensors", hash="same") |
| 18 | + second = create_content(session, "/models/b.safetensors", hash="same") |
184 | 19 |
|
185 | | - update_asset_hash_and_mime(session, asset_id=asset.id, mime_type=update_mime, asset_hash=update_hash) |
186 | | - assert asset.mime_type == expected_mime |
187 | | - assert asset.hash == expected_hash |
| 20 | + assert first.id != second.id |
0 commit comments