Files
bambuddy/backend/tests/unit/test_makerworld_routes.py
T
maziggy 54389a54aa fix(library): MakerWorld URL import honours external folder destinations (#1645)
Reported and root-caused by @needo37. Importing a model via the MakerWorld
  URL-download feature into a writable external folder (e.g. SMB/NFS-mounted
  NAS) saved the 3MF into Bambuddy's internal managed library dir, not the
  external mount. The file card showed in the File Manager under the
  external folder, but the bytes never landed on the NAS, and the on-disk
  copy was UUID-renamed so a find by the original basename matched nothing.

  Root cause was save_3mf_bytes_to_library at backend/app/api/routes/library.py:422:
  it accepted folder_id but never loaded the folder, never inspected
  is_external / external_path, hardcoded the destination to
  get_library_files_dir() with a UUID name, and left the LibraryFile row
  with is_external=False. So the row's folder_id pointed at the external
  folder while its bytes and is_external flag both said "managed/internal".
  Same class of bug as #1112, which had been fixed for the multipart-upload
  and move paths but never applied to this byte-import path.

  Fix mirrors the multipart-upload path directly:
  - Load target_folder from folder_id when non-None.
  - Feed it to _resolve_upload_destination(target_folder, filename), which
    already returns (dest, is_external) and enforces the 403-read-only /
    400-unwritable-or-missing / 409-collision rejections.
  - Write bytes to dest (real filename for external, UUID for managed).
  - Persist the row with file_path=_stored_file_path(dest, is_external)
    and is_external=is_external.

  The route-layer read-only guard at makerworld.py:256-260 is preserved -
  it returns the friendlier error before the upstream download burns
  bandwidth - and _resolve_upload_destination's identical check stays as
  defence-in-depth for any future caller that skips the route gate.
  Thumbnails continue to live under the managed get_library_thumbnails_dir()
  regardless of the 3MF's location, matching the upload path.
2026-06-05 09:29:53 +02:00

678 lines
28 KiB
Python

"""Tests for the /makerworld/* route handlers.
Mocks ``MakerWorldService`` so tests don't hit the real MakerWorld API. We
still cover: URL validation, metadata passthrough, already-imported detection,
source-URL-based dedupe on import, auto-creation of the MakerWorld default
folder, canonical URL shape, filename basenaming, and the ``/recent-imports``
listing endpoint.
"""
from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, patch
import pytest
from backend.app.api.routes.makerworld import _canonical_url
from backend.app.models.library import LibraryFile, LibraryFolder
def _fake_service(**stubs):
"""Build an AsyncMock MakerWorldService with the given async method stubs."""
svc = AsyncMock()
svc.close = AsyncMock()
for name, value in stubs.items():
if callable(value) and not isinstance(value, AsyncMock):
setattr(svc, name, AsyncMock(side_effect=value))
else:
setattr(svc, name, AsyncMock(return_value=value))
return svc
def _default_design(alphanumeric: str = "US2bb73b106683e5", model_id: int = 1400373):
"""Shape the backend needs from ``/design/{id}``: the alphanumeric
``modelId`` field that iot-service requires, plus at least one instance
so the importer has a ``profile_id`` to fall back on."""
return {
"id": model_id,
"modelId": alphanumeric,
"title": "Seed Starter",
"instances": [{"profileId": 298919107, "title": "9 cells"}],
}
def _default_manifest(name: str = "benchy.3mf"):
return {
"name": name,
"url": "https://makerworld.bblmw.com/makerworld/model/X/Y/f.3mf?exp=1&key=k",
}
class TestCanonicalUrl:
"""Unit test the dedupe-key builder directly — regressions break dedupe
silently so it's worth pinning the exact shape."""
def test_without_profile_id(self):
assert _canonical_url(1400373) == "https://makerworld.com/models/1400373"
def test_without_profile_id_when_none(self):
assert _canonical_url(1400373, None) == "https://makerworld.com/models/1400373"
def test_with_profile_id(self):
assert _canonical_url(1400373, 298919107) == ("https://makerworld.com/models/1400373#profileId-298919107")
class TestStatus:
@pytest.mark.asyncio
async def test_status_reports_no_token_by_default(self, async_client, db_session):
resp = await async_client.get("/api/v1/makerworld/status")
assert resp.status_code == 200
body = resp.json()
# Fresh in-memory DB has no stored token, so can_download must be false
assert body == {"has_cloud_token": False, "can_download": False}
class TestResolve:
@pytest.mark.asyncio
async def test_rejects_non_makerworld_url(self, async_client):
resp = await async_client.post(
"/api/v1/makerworld/resolve",
json={"url": "https://thingiverse.com/thing/1"},
)
assert resp.status_code == 400
assert "makerworld" in resp.json()["detail"].lower()
@pytest.mark.asyncio
async def test_happy_path_returns_design_and_instances(self, async_client):
design_payload = {"id": 1400373, "title": "Seed Starter"}
instances_payload = {
"total": 2,
"hits": [
{"id": 1452154, "profileId": 298919107, "title": "9 cells"},
{"id": 1452158, "profileId": 298919564, "title": "12 cells"},
],
}
svc = _fake_service(get_design=design_payload, get_design_instances=instances_payload)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/resolve",
json={"url": "https://makerworld.com/en/models/1400373-slug#profileId-1452154"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["model_id"] == 1400373
assert body["profile_id"] == 1452154
assert body["design"] == design_payload
assert len(body["instances"]) == 2
assert body["already_imported_library_ids"] == []
@pytest.mark.asyncio
async def test_flags_already_imported_library_ids(self, async_client, db_session):
# Seed a matching LibraryFile so resolve() reports it back
existing = LibraryFile(
filename="prev.3mf",
file_path="library/files/prev.3mf",
file_type="3mf",
file_size=100,
source_type="makerworld",
source_url="https://makerworld.com/models/1400373",
)
db_session.add(existing)
await db_session.commit()
await db_session.refresh(existing)
svc = _fake_service(
get_design={"id": 1400373},
get_design_instances={"total": 0, "hits": []},
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/resolve",
json={"url": "https://makerworld.com/en/models/1400373"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["already_imported_library_ids"] == [existing.id]
@pytest.mark.asyncio
async def test_merges_compatibility_from_design_into_instances(self, async_client):
"""Per-instance printer compatibility info lives on
``design.instances[].extention.modelInfo`` but not on
``/instances/hits``. Resolve enriches each hit with both
``compatibility`` (primary printer the instance was sliced for) and
``otherCompatibility`` (extra printers the uploader marked it
compatible with) so the frontend can show "sliced for A1 / also
marked compatible with: H2D, P1S".
"""
design_payload = {
"id": 1400373,
"title": "Seed Starter",
"instances": [
{
"id": 1452154,
"extention": {
"modelInfo": {
"compatibility": ["A1"],
"otherCompatibility": ["H2D", "P1S"],
}
},
},
{
"id": 1452158,
"extention": {
"modelInfo": {
"compatibility": ["X1 Carbon"],
"otherCompatibility": [],
}
},
},
],
}
instances_payload = {
"total": 2,
"hits": [
{"id": 1452154, "profileId": 298919107, "title": "9 cells"},
{"id": 1452158, "profileId": 298919564, "title": "12 cells"},
],
}
svc = _fake_service(get_design=design_payload, get_design_instances=instances_payload)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/resolve",
json={"url": "https://makerworld.com/en/models/1400373"},
)
assert resp.status_code == 200, resp.text
instances = resp.json()["instances"]
by_id = {i["id"]: i for i in instances}
assert by_id[1452154]["compatibility"] == ["A1"]
assert by_id[1452154]["otherCompatibility"] == ["H2D", "P1S"]
assert by_id[1452158]["compatibility"] == ["X1 Carbon"]
assert by_id[1452158]["otherCompatibility"] == []
@pytest.mark.asyncio
async def test_resolve_handles_missing_compatibility_gracefully(self, async_client):
"""Older designs (or hits without a matching design.instances entry)
must not crash the resolve response — they just don't get the
compat fields."""
design_payload = {"id": 1400373, "instances": [{"id": 1452154}]} # no extention
instances_payload = {
"total": 2,
"hits": [
{"id": 1452154, "profileId": 298919107},
{"id": 9999999, "profileId": 298919999}, # no design.instances match
],
}
svc = _fake_service(get_design=design_payload, get_design_instances=instances_payload)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/resolve",
json={"url": "https://makerworld.com/en/models/1400373"},
)
assert resp.status_code == 200, resp.text
instances = resp.json()["instances"]
# First instance: design entry exists but no extention → fields absent or None.
first = next(i for i in instances if i["id"] == 1452154)
assert first.get("compatibility") is None
assert first.get("otherCompatibility") is None
# Second instance: no design entry at all → no enrichment, no crash.
second = next(i for i in instances if i["id"] == 9999999)
assert "compatibility" not in second or second["compatibility"] is None
class TestImport:
"""End-to-end of POST /makerworld/import — mocks the service but exercises
real DB writes, real ``save_3mf_bytes_to_library``, real folder auto-creation."""
_FAKE_3MF_BYTES = b"PK\x03\x04not-a-real-3mf"
@pytest.mark.asyncio
async def test_returns_existing_on_source_url_match(self, async_client, db_session):
"""Re-importing a model we already have must NOT re-download.
Dedupe key is ``{model_id}#profileId-{profile_id}`` — matches the
canonical URL the route constructs, not the legacy model-only shape.
"""
existing = LibraryFile(
filename="already-here.3mf",
file_path="library/files/already.3mf",
file_type="3mf",
file_size=500,
source_type="makerworld",
source_url="https://makerworld.com/models/1400373#profileId-298919107",
)
db_session.add(existing)
await db_session.commit()
await db_session.refresh(existing)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
)
svc.download_3mf = AsyncMock() # must remain uncalled
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["library_file_id"] == existing.id
assert body["was_existing"] is True
assert body["profile_id"] == 298919107
svc.download_3mf.assert_not_called()
@pytest.mark.asyncio
async def test_autocreates_makerworld_folder_when_folder_id_none(self, async_client, db_session):
"""Default destination — a top-level "MakerWorld" folder — is created
on first import so users don't have to set it up."""
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": None},
)
assert resp.status_code == 200, resp.text
# The new folder should exist, at the root.
from sqlalchemy import select
result = await db_session.execute(
select(LibraryFolder).where(LibraryFolder.name == "MakerWorld", LibraryFolder.parent_id.is_(None))
)
folder = result.scalar_one()
assert resp.json()["folder_id"] == folder.id
@pytest.mark.asyncio
async def test_uses_existing_folder_when_folder_id_provided(self, async_client, db_session):
"""Caller-supplied ``folder_id`` must be honoured even if the default
``MakerWorld`` folder also exists — no silent hijacking."""
folder = LibraryFolder(name="MyCustomFolder", parent_id=None)
db_session.add(folder)
await db_session.commit()
await db_session.refresh(folder)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": folder.id},
)
assert resp.status_code == 200, resp.text
assert resp.json()["folder_id"] == folder.id
@pytest.mark.asyncio
async def test_canonical_source_url_includes_profile_id(self, async_client, db_session):
"""The saved row's ``source_url`` must include ``#profileId-`` so two
plates of the same model become two library rows (dedupe is per-plate)."""
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107},
)
assert resp.status_code == 200, resp.text
from sqlalchemy import select
row = (
await db_session.execute(select(LibraryFile).where(LibraryFile.id == resp.json()["library_file_id"]))
).scalar_one()
assert row.source_url == "https://makerworld.com/models/1400373#profileId-298919107"
@pytest.mark.asyncio
async def test_filename_from_upstream_is_basenamed(self, async_client, db_session):
"""Defence-in-depth: a malicious ``name`` from the upstream manifest
(e.g. ``"../../evil.3mf"``) must not persist path components into the
library row. On-disk storage uses a UUID already, this is belt-and-
braces protection for the human-readable field."""
svc = _fake_service(
get_design=_default_design(),
get_profile_download={
"name": "../../evil.3mf",
"url": "https://makerworld.bblmw.com/makerworld/model/X/Y/f.3mf?exp=1&key=k",
},
download_3mf=(self._FAKE_3MF_BYTES, "fallback.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107},
)
assert resp.status_code == 200, resp.text
assert resp.json()["filename"] == "evil.3mf"
@pytest.mark.asyncio
async def test_response_includes_profile_id(self, async_client, db_session):
"""UI matches imports back to the plate row via ``profile_id`` — the
response field must always be populated, even when the caller provided
it explicitly (rather than the backend falling back to design defaults)."""
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107},
)
assert resp.status_code == 200, resp.text
assert resp.json()["profile_id"] == 298919107
@pytest.mark.asyncio
async def test_import_to_writable_external_writes_bytes_to_mount(self, async_client, db_session, tmp_path):
"""#1645: importing into a writable external folder writes the bytes to
``<external_path>/<filename>`` and tags the row ``is_external=True`` —
same shape as the multipart-upload path (#1112). Previously the bytes
landed in the internal library dir under a UUID name while the row
showed up under the external folder in the UI, leaving a NAS/SMB user
unable to find their file on the mount."""
ext_dir = tmp_path / "nas-makerworld"
ext_dir.mkdir()
folder = LibraryFolder(
name="NAS Imports",
parent_id=None,
is_external=True,
external_path=str(ext_dir),
external_readonly=False,
)
db_session.add(folder)
await db_session.commit()
await db_session.refresh(folder)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest("seed-starter.3mf"),
download_3mf=(self._FAKE_3MF_BYTES, "seed-starter.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": folder.id},
)
assert resp.status_code == 200, resp.text
from sqlalchemy import select
row = (
await db_session.execute(select(LibraryFile).where(LibraryFile.id == resp.json()["library_file_id"]))
).scalar_one()
assert row.folder_id == folder.id
assert row.is_external is True, "Row must be tagged external so re-scan can reconcile it"
# External rows persist the absolute mount path (matches scan + upload paths).
assert row.file_path == str(ext_dir / "seed-starter.3mf")
on_disk = ext_dir / "seed-starter.3mf"
assert on_disk.is_file(), "Bytes must land on the external mount, not in the internal library dir"
assert on_disk.read_bytes() == self._FAKE_3MF_BYTES
@pytest.mark.asyncio
async def test_import_to_readonly_external_rejected_at_route(self, async_client, db_session, tmp_path):
"""The route-layer gate at ``makerworld.py:256-260`` rejects read-only
externals with 403 before any download happens — so MakerWorld
credentials and the upstream download bandwidth aren't wasted."""
ext_dir = tmp_path / "nas-readonly"
ext_dir.mkdir()
folder = LibraryFolder(
name="NAS read-only",
parent_id=None,
is_external=True,
external_path=str(ext_dir),
external_readonly=True,
)
db_session.add(folder)
await db_session.commit()
await db_session.refresh(folder)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
)
svc.download_3mf = AsyncMock()
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": folder.id},
)
assert resp.status_code == 403, resp.text
svc.download_3mf.assert_not_called()
@pytest.mark.asyncio
async def test_import_to_external_with_missing_path_returns_400(self, async_client, db_session, tmp_path):
"""If the external folder's mount has gone away (NAS unplugged, SMB
share down), ``_resolve_upload_destination`` returns 400 before the
write so we don't silently fall back to the internal library dir."""
missing_dir = tmp_path / "vanished-mount" # NOTE: deliberately not created
folder = LibraryFolder(
name="NAS gone",
parent_id=None,
is_external=True,
external_path=str(missing_dir),
external_readonly=False,
)
db_session.add(folder)
await db_session.commit()
await db_session.refresh(folder)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest(),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": folder.id},
)
assert resp.status_code == 400, resp.text
assert "not accessible" in resp.text.lower()
@pytest.mark.asyncio
async def test_import_to_external_with_name_collision_returns_409(self, async_client, db_session, tmp_path):
"""A user-visible 409 fires when the filename already exists on the
external mount, instead of silently overwriting a file the user put
there outside Bambuddy."""
ext_dir = tmp_path / "nas-collide"
ext_dir.mkdir()
(ext_dir / "benchy.3mf").write_bytes(b"pre-existing")
folder = LibraryFolder(
name="NAS collide",
parent_id=None,
is_external=True,
external_path=str(ext_dir),
external_readonly=False,
)
db_session.add(folder)
await db_session.commit()
await db_session.refresh(folder)
svc = _fake_service(
get_design=_default_design(),
get_profile_download=_default_manifest("benchy.3mf"),
download_3mf=(self._FAKE_3MF_BYTES, "benchy.3mf"),
)
with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
resp = await async_client.post(
"/api/v1/makerworld/import",
json={"model_id": 1400373, "profile_id": 298919107, "folder_id": folder.id},
)
assert resp.status_code == 409, resp.text
# Pre-existing file's contents must not be clobbered by the failed write.
assert (ext_dir / "benchy.3mf").read_bytes() == b"pre-existing"
class TestRecentImports:
"""GET /makerworld/recent-imports — sidebar feed on the MakerWorld page."""
@pytest.mark.asyncio
async def test_empty_when_no_makerworld_imports(self, async_client):
resp = await async_client.get("/api/v1/makerworld/recent-imports")
assert resp.status_code == 200
assert resp.json() == []
@pytest.mark.asyncio
async def test_returns_items_newest_first(self, async_client, db_session):
# Seed three rows with explicit, decreasing created_at timestamps so
# ordering doesn't depend on auto-increment PK ordering.
base = datetime(2025, 1, 1, 12, 0, 0)
older = LibraryFile(
filename="older.3mf",
file_path="library/older.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url="https://makerworld.com/models/1",
created_at=base,
)
middle = LibraryFile(
filename="middle.3mf",
file_path="library/middle.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url="https://makerworld.com/models/2",
created_at=base + timedelta(hours=1),
)
newer = LibraryFile(
filename="newer.3mf",
file_path="library/newer.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url="https://makerworld.com/models/3",
created_at=base + timedelta(hours=2),
)
# Unrelated non-MakerWorld file must NOT show up.
other = LibraryFile(
filename="manual.3mf",
file_path="library/manual.3mf",
file_type="3mf",
file_size=10,
source_type=None,
source_url=None,
created_at=base + timedelta(hours=3),
)
db_session.add_all([older, middle, newer, other])
await db_session.commit()
resp = await async_client.get("/api/v1/makerworld/recent-imports")
assert resp.status_code == 200, resp.text
body = resp.json()
names = [row["filename"] for row in body]
assert names == ["newer.3mf", "middle.3mf", "older.3mf"]
@pytest.mark.asyncio
async def test_response_matches_pydantic_shape(self, async_client, db_session):
"""Lock the exact key set so the frontend's typed ``MakerworldRecentImport``
doesn't silently fall out of sync with the backend schema."""
row = LibraryFile(
filename="x.3mf",
file_path="library/x.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url="https://makerworld.com/models/1#profileId-2",
)
db_session.add(row)
await db_session.commit()
resp = await async_client.get("/api/v1/makerworld/recent-imports")
assert resp.status_code == 200, resp.text
item = resp.json()[0]
assert set(item.keys()) == {
"library_file_id",
"filename",
"folder_id",
"thumbnail_path",
"source_url",
"created_at",
}
assert item["source_url"] == "https://makerworld.com/models/1#profileId-2"
@pytest.mark.asyncio
async def test_limit_is_honoured(self, async_client, db_session):
for i in range(5):
db_session.add(
LibraryFile(
filename=f"f{i}.3mf",
file_path=f"library/f{i}.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url=f"https://makerworld.com/models/{i}",
)
)
await db_session.commit()
resp = await async_client.get("/api/v1/makerworld/recent-imports?limit=2")
assert resp.status_code == 200
assert len(resp.json()) == 2
@pytest.mark.asyncio
async def test_limit_clamped_to_minimum(self, async_client, db_session):
"""``limit=0`` or negative must clamp to 1 — a zero limit would be
silently swallowed by SQL and return nothing, which is surprising."""
db_session.add(
LibraryFile(
filename="one.3mf",
file_path="library/one.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url="https://makerworld.com/models/1",
)
)
await db_session.commit()
resp = await async_client.get("/api/v1/makerworld/recent-imports?limit=0")
assert resp.status_code == 200
assert len(resp.json()) == 1
@pytest.mark.asyncio
async def test_limit_clamped_to_maximum(self, async_client, db_session):
"""``limit`` is clamped to 50 so a pathological client can't request
the whole table. We seed 60 rows and assert the response is capped."""
for i in range(60):
db_session.add(
LibraryFile(
filename=f"f{i}.3mf",
file_path=f"library/f{i}.3mf",
file_type="3mf",
file_size=10,
source_type="makerworld",
source_url=f"https://makerworld.com/models/{i}",
)
)
await db_session.commit()
resp = await async_client.get("/api/v1/makerworld/recent-imports?limit=9999")
assert resp.status_code == 200
assert len(resp.json()) == 50