test_slicer_filament_resolver.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. """Tests for ``resolve_slicer_filament`` (#1815).
  2. The defensive filter at the end of the resolver clears ``tray_info_idx``
  3. when its value isn't slicer-acceptable (literal material names + PFUS /
  4. PFCN cloud-preset prefixes that the printer's calibration table can't
  5. key on). Pre-#1815 it cleared ``setting_id`` alongside, which dropped
  6. the slicer's only handle on the user's actual custom preset and forced
  7. the caller into the generic-material fallback — Bambu Studio then
  8. displayed "Generic <Material>" for spools whose Bambu Cloud detail
  9. lookup didn't resolve a ``filament_id`` (cloud unauth on the on_ams_change
  10. replay path, transient cloud failure, or custom presets whose detail
  11. JSON omits ``filament_id``).
  12. Post-#1815 the filter preserves a setting_id that's still a valid
  13. slicer reference (PFUS / PFCN cloud user/shared preset, or GFS Bambu
  14. official preset) even when ``tray_info_idx`` is cleared.
  15. """
  16. from __future__ import annotations
  17. from unittest.mock import AsyncMock, MagicMock, patch
  18. import pytest
  19. from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
  20. @pytest.mark.asyncio
  21. async def test_pfus_cloud_unavailable_preserves_setting_id():
  22. """Reporter scenario: PFUS cloud user preset, cloud lookup fails to
  23. return a filament_id. setting_id must survive so the slicer can
  24. still find the user's actual custom preset."""
  25. db = MagicMock()
  26. with patch(
  27. "backend.app.api.routes.cloud.build_authenticated_cloud",
  28. AsyncMock(return_value=None),
  29. ):
  30. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  31. db=db,
  32. current_user=None,
  33. slicer_filament="PFUS990b6e19965353",
  34. slicer_filament_name="Jayo PETG HF",
  35. material="PETG",
  36. )
  37. assert tray_info_idx == ""
  38. assert setting_id == "PFUS990b6e19965353"
  39. assert sub_brand is None
  40. @pytest.mark.asyncio
  41. async def test_pfcn_cloud_unavailable_preserves_setting_id():
  42. """PFCN partner/shared cloud preset (e.g. Polymaker H2D variants,
  43. #1648) shares the same shape problem as PFUS."""
  44. db = MagicMock()
  45. with patch(
  46. "backend.app.api.routes.cloud.build_authenticated_cloud",
  47. AsyncMock(return_value=None),
  48. ):
  49. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  50. db=db,
  51. current_user=None,
  52. slicer_filament="PFCN1234567890",
  53. slicer_filament_name="Polymaker PolyTerra PLA",
  54. material="PLA",
  55. )
  56. assert tray_info_idx == ""
  57. assert setting_id == "PFCN1234567890"
  58. assert sub_brand is None
  59. @pytest.mark.asyncio
  60. async def test_pfus_cloud_resolves_filament_id_regression_guard():
  61. """When cloud auth works and returns a filament_id, the resolver
  62. keeps its existing behaviour: tray_info_idx = real filament_id,
  63. setting_id = original PFUS reference."""
  64. db = MagicMock()
  65. cloud_mock = MagicMock()
  66. cloud_mock.is_authenticated = True
  67. cloud_mock.get_setting_detail = AsyncMock(return_value={"filament_id": "P285e239", "name": "Jayo PETG HF @P1S"})
  68. cloud_mock.close = AsyncMock()
  69. with patch(
  70. "backend.app.api.routes.cloud.build_authenticated_cloud",
  71. AsyncMock(return_value=cloud_mock),
  72. ):
  73. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  74. db=db,
  75. current_user=MagicMock(),
  76. slicer_filament="PFUS990b6e19965353",
  77. slicer_filament_name="Jayo PETG HF",
  78. material="PETG",
  79. )
  80. assert tray_info_idx == "P285e239"
  81. assert setting_id == "PFUS990b6e19965353"
  82. assert sub_brand == "Jayo PETG HF"
  83. @pytest.mark.asyncio
  84. async def test_gfs_cloud_unavailable_resolves_via_normalize():
  85. """GFS Bambu official preset + cloud unavailable: normalize strips
  86. the 'S' to give a real filament_id ('GFG02'), so tray_info_idx is
  87. valid and the defensive filter doesn't trigger. setting_id stays as
  88. the original GFS reference. Regression guard for the cloud-down
  89. Bambu-official path."""
  90. db = MagicMock()
  91. with patch(
  92. "backend.app.api.routes.cloud.build_authenticated_cloud",
  93. AsyncMock(return_value=None),
  94. ):
  95. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  96. db=db,
  97. current_user=None,
  98. slicer_filament="GFSG02",
  99. slicer_filament_name=None,
  100. material="PETG",
  101. )
  102. assert tray_info_idx == "GFG02"
  103. assert setting_id == "GFSG02"
  104. assert sub_brand is None
  105. @pytest.mark.asyncio
  106. async def test_literal_material_name_clears_both():
  107. """slicer_filament='PETG' (free-text material leak from legacy
  108. spools): both tray_info_idx and setting_id must be cleared so the
  109. caller's generic-material fallback rescues the slot. Regression
  110. guard that the PFUS preservation doesn't accidentally preserve
  111. literal material names."""
  112. db = MagicMock()
  113. with patch(
  114. "backend.app.api.routes.cloud.build_authenticated_cloud",
  115. AsyncMock(return_value=None),
  116. ):
  117. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  118. db=db,
  119. current_user=None,
  120. slicer_filament="PETG",
  121. slicer_filament_name=None,
  122. material="PETG",
  123. )
  124. assert tray_info_idx == ""
  125. assert setting_id == ""
  126. assert sub_brand is None
  127. class TestThePresetsOwnType:
  128. """#2902: a preset is chosen from a list the slicer defines, so its
  129. ``filament_type`` is the slicer's own answer to what the material is --
  130. no reading of a product name required. Raised by @doncaruana on the issue
  131. after the first fix reduced "PLA Aero" to "PLA".
  132. The resolver hands that answer back as the fourth element; the two assign
  133. routes write it into ``tray_type`` in preference to reducing the spool's
  134. material column. ``None`` means no preset said, and the reduction stands.
  135. """
  136. @pytest.mark.asyncio
  137. async def test_a_local_presets_type_is_returned(self):
  138. db = MagicMock()
  139. lp = MagicMock()
  140. lp.filament_type = "PLA-AERO"
  141. lp.setting = None
  142. lp.name = "Bambu PLA Aero @BBL X1C"
  143. result = MagicMock()
  144. result.scalar_one_or_none = MagicMock(return_value=lp)
  145. db.execute = AsyncMock(return_value=result)
  146. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  147. db=db,
  148. current_user=None,
  149. slicer_filament="38",
  150. slicer_filament_name=None,
  151. material="PLA",
  152. )
  153. assert type_override == "PLA-AERO"
  154. @pytest.mark.asyncio
  155. async def test_a_cloud_presets_type_is_read_out_of_its_profile(self):
  156. """Both slicers store it as a one-element array, and the preset JSON
  157. sits under ``setting`` in the cloud envelope."""
  158. db = MagicMock()
  159. cloud = MagicMock()
  160. cloud.is_authenticated = True
  161. cloud.get_setting_detail = AsyncMock(
  162. return_value={
  163. "filament_id": "GFA11",
  164. "name": "Bambu PLA Aero @BBL X1C",
  165. "setting": {"filament_type": ["PLA-AERO"]},
  166. }
  167. )
  168. cloud.close = AsyncMock()
  169. with patch(
  170. "backend.app.api.routes.cloud.build_authenticated_cloud",
  171. AsyncMock(return_value=cloud),
  172. ):
  173. idx, _sid, _brand, type_override = await resolve_slicer_filament(
  174. db=db,
  175. current_user=None,
  176. slicer_filament="GFSA11",
  177. slicer_filament_name=None,
  178. material="PLA",
  179. )
  180. assert idx == "GFA11"
  181. assert type_override == "PLA-AERO"
  182. @pytest.mark.asyncio
  183. async def test_a_bare_string_filament_type_is_accepted_too(self):
  184. """Hand-written and older profiles store it unwrapped. ``orca_profiles``
  185. accepts both forms, so this has to as well."""
  186. db = MagicMock()
  187. cloud = MagicMock()
  188. cloud.is_authenticated = True
  189. cloud.get_setting_detail = AsyncMock(
  190. return_value={"filament_id": "GFG02", "setting": {"filament_type": "PETG"}}
  191. )
  192. cloud.close = AsyncMock()
  193. with patch(
  194. "backend.app.api.routes.cloud.build_authenticated_cloud",
  195. AsyncMock(return_value=cloud),
  196. ):
  197. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  198. db=db,
  199. current_user=None,
  200. slicer_filament="GFSG02",
  201. slicer_filament_name=None,
  202. material="PETG",
  203. )
  204. assert type_override == "PETG"
  205. @pytest.mark.asyncio
  206. async def test_no_preset_means_no_answer(self):
  207. """A spool with no slicer_filament -- the case this issue was reported
  208. for. ``material`` is required on a spool and ``slicer_filament`` is
  209. not, so the reduction has to stay as the fallback."""
  210. db = MagicMock()
  211. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  212. db=db,
  213. current_user=None,
  214. slicer_filament=None,
  215. slicer_filament_name=None,
  216. material="PLA+",
  217. )
  218. assert type_override is None
  219. @pytest.mark.asyncio
  220. async def test_a_preset_that_does_not_say_gets_no_opinion(self):
  221. db = MagicMock()
  222. cloud = MagicMock()
  223. cloud.is_authenticated = True
  224. cloud.get_setting_detail = AsyncMock(return_value={"filament_id": "GFG02", "setting": {}})
  225. cloud.close = AsyncMock()
  226. with patch(
  227. "backend.app.api.routes.cloud.build_authenticated_cloud",
  228. AsyncMock(return_value=cloud),
  229. ):
  230. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  231. db=db,
  232. current_user=None,
  233. slicer_filament="GFSG02",
  234. slicer_filament_name=None,
  235. material="PETG",
  236. )
  237. assert type_override is None
  238. class TestACustomPresetsOwnFilamentId:
  239. """Where the id that carries a custom preset into an AMS slot comes from.
  240. The slot holds one filament reference and the printer truncates it to 8
  241. characters, so a custom preset reaches the slicer as itself only when its
  242. own filament_id ("P" + 7 hex) goes into ``tray_info_idx``. 92 trays across
  243. eight models in the support archive do exactly that, so the mechanism
  244. works -- what #3003 found is that we only ever read one of the two places
  245. Bambu Cloud returns that id from.
  246. """
  247. @pytest.mark.asyncio
  248. async def test_filament_id_is_read_from_inside_the_preset_json(self):
  249. """The envelope has none, the preset JSON does -- and it wins over base_id.
  250. Before #3003 this fell through to the base_id branch and the slot came
  251. out as the Bambu profile the custom preset inherits from.
  252. """
  253. db = MagicMock()
  254. cloud = MagicMock()
  255. cloud.is_authenticated = True
  256. cloud.get_setting_detail = AsyncMock(
  257. return_value={
  258. "name": "SUNLU PLA Transparent @BBL A1",
  259. "base_id": "GFSNLS03",
  260. "setting": {"filament_id": "P4d64437", "filament_type": ["PLA"]},
  261. }
  262. )
  263. cloud.close = AsyncMock()
  264. with patch(
  265. "backend.app.api.routes.cloud.build_authenticated_cloud",
  266. AsyncMock(return_value=cloud),
  267. ):
  268. idx, sid, brand, _type = await resolve_slicer_filament(
  269. db=db,
  270. current_user=None,
  271. slicer_filament="PFUSfb87cd50b76616",
  272. slicer_filament_name=None,
  273. material="PLA",
  274. )
  275. assert idx == "P4d64437"
  276. assert sid == "PFUSfb87cd50b76616"
  277. assert brand == "SUNLU PLA Transparent"
  278. @pytest.mark.asyncio
  279. async def test_the_envelope_still_wins_when_it_has_one(self):
  280. """Unchanged behaviour for the presets that already resolved."""
  281. db = MagicMock()
  282. cloud = MagicMock()
  283. cloud.is_authenticated = True
  284. cloud.get_setting_detail = AsyncMock(
  285. return_value={
  286. "filament_id": "P285e239",
  287. "name": "Jayo PETG HF @P1S",
  288. "base_id": "GFSG02",
  289. "setting": {"filament_id": "P999aaaa"},
  290. }
  291. )
  292. cloud.close = AsyncMock()
  293. with patch(
  294. "backend.app.api.routes.cloud.build_authenticated_cloud",
  295. AsyncMock(return_value=cloud),
  296. ):
  297. idx, _sid, _brand, _type = await resolve_slicer_filament(
  298. db=db,
  299. current_user=None,
  300. slicer_filament="PFUS992454068158eb",
  301. slicer_filament_name=None,
  302. material="PETG",
  303. )
  304. assert idx == "P285e239"
  305. @pytest.mark.asyncio
  306. async def test_base_id_is_still_the_fallback_when_neither_place_has_one(self):
  307. """A preset with no filament_id of its own genuinely is its base, and
  308. the base id is storable, so it is the right answer -- just not one to
  309. reach for while the preset's own id is sitting under ``setting``."""
  310. db = MagicMock()
  311. cloud = MagicMock()
  312. cloud.is_authenticated = True
  313. cloud.get_setting_detail = AsyncMock(
  314. return_value={"name": "My PLA @BBL A1", "base_id": "GFSNLS03", "setting": {}}
  315. )
  316. cloud.close = AsyncMock()
  317. with patch(
  318. "backend.app.api.routes.cloud.build_authenticated_cloud",
  319. AsyncMock(return_value=cloud),
  320. ):
  321. idx, sid, _brand, _type = await resolve_slicer_filament(
  322. db=db,
  323. current_user=None,
  324. slicer_filament="PFUSfb87cd50b76616",
  325. slicer_filament_name=None,
  326. material="PLA",
  327. )
  328. # SUNLU PLA+'s own id: its setting_id does not follow the "drop the S"
  329. # rule, which gave "GFNLS03", an id no profile has (#3387).
  330. assert idx == "GFSNL03"
  331. assert sid == "PFUSfb87cd50b76616"
  332. class TestOrcaCloudIsTheFirstSource:
  333. """Source order is Orca Cloud, Bambu Cloud, local import, generic.
  334. Orca was absent from the resolver entirely: a spool referencing an Orca
  335. profile stores the bare UUID, which matched no branch and fell through
  336. ``normalize_slicer_filament`` -- a function that passes anything it does
  337. not recognise straight through. The UUID reached tray_info_idx, a field
  338. the printer truncates to 8 characters (#3003).
  339. """
  340. ORCA_ID = "3f2a9c1e-4b7d-4a02-9f61-8c5e2d1a7b30"
  341. @staticmethod
  342. def _svc(profile):
  343. svc = MagicMock()
  344. svc.get_profile = AsyncMock(return_value=profile)
  345. svc.close = AsyncMock()
  346. return svc
  347. @pytest.mark.asyncio
  348. async def test_the_profiles_own_filament_id_is_used(self):
  349. db = MagicMock()
  350. svc = self._svc(
  351. {
  352. "id": self.ORCA_ID,
  353. "name": "Overture Matte PLA @Orca",
  354. "content": {"filament_id": "P56e1be0", "filament_type": ["PLA"]},
  355. }
  356. )
  357. with patch(
  358. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  359. AsyncMock(return_value=svc),
  360. ):
  361. idx, sid, brand, type_override = await resolve_slicer_filament(
  362. db=db,
  363. current_user=None,
  364. slicer_filament=self.ORCA_ID,
  365. slicer_filament_name=None,
  366. material="PLA",
  367. )
  368. assert idx == "P56e1be0"
  369. # The UUID is foreign to the slicer in either field, so nothing carries it.
  370. assert sid == ""
  371. assert brand == "Overture Matte PLA"
  372. assert type_override == "PLA"
  373. svc.close.assert_awaited()
  374. @pytest.mark.asyncio
  375. async def test_a_profile_with_no_filament_id_leaves_the_caller_its_fallback(self):
  376. db = MagicMock()
  377. svc = self._svc({"id": self.ORCA_ID, "name": "My PLA", "content": {"filament_type": ["PLA"]}})
  378. with patch(
  379. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  380. AsyncMock(return_value=svc),
  381. ):
  382. idx, sid, _brand, _type = await resolve_slicer_filament(
  383. db=db,
  384. current_user=None,
  385. slicer_filament=self.ORCA_ID,
  386. slicer_filament_name=None,
  387. material="PLA",
  388. )
  389. assert idx == ""
  390. assert sid == ""
  391. @pytest.mark.asyncio
  392. async def test_an_unreachable_orca_never_leaks_the_uuid(self):
  393. """No pairing, dead token, Orca down -- all the same answer. The UUID
  394. must not reach tray_info_idx, which is what happened before the branch
  395. existed at all."""
  396. db = MagicMock()
  397. with patch(
  398. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  399. AsyncMock(side_effect=RuntimeError("Orca Cloud is not connected")),
  400. ):
  401. idx, sid, _brand, _type = await resolve_slicer_filament(
  402. db=db,
  403. current_user=None,
  404. slicer_filament=self.ORCA_ID,
  405. slicer_filament_name=None,
  406. material="PLA",
  407. )
  408. assert idx == ""
  409. assert sid == ""
  410. @pytest.mark.asyncio
  411. async def test_a_caller_without_the_permission_skips_the_lookup(self):
  412. db = MagicMock()
  413. user = MagicMock()
  414. user.has_permission = MagicMock(return_value=False)
  415. with patch(
  416. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  417. AsyncMock(side_effect=AssertionError("must not be called")),
  418. ):
  419. idx, _sid, _brand, _type = await resolve_slicer_filament(
  420. db=db,
  421. current_user=user,
  422. slicer_filament=self.ORCA_ID,
  423. slicer_filament_name=None,
  424. material="PLA",
  425. )
  426. assert idx == ""
  427. @pytest.mark.asyncio
  428. async def test_a_uuid_is_refused_as_tray_info_idx_by_the_closing_guard(self):
  429. """Belt and braces: a profile whose content names itself by UUID still
  430. does not put one in the field."""
  431. db = MagicMock()
  432. svc = self._svc({"id": self.ORCA_ID, "name": "Odd", "content": {"filament_id": self.ORCA_ID}})
  433. with patch(
  434. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  435. AsyncMock(return_value=svc),
  436. ):
  437. idx, sid, _brand, _type = await resolve_slicer_filament(
  438. db=db,
  439. current_user=None,
  440. slicer_filament=self.ORCA_ID,
  441. slicer_filament_name=None,
  442. material="PLA",
  443. )
  444. assert idx == ""
  445. assert sid == ""
  446. class TestOrcaLookupFollowsParents:
  447. """``lookup_orca_filament_id`` (#3216).
  448. A profile that inherits carries only its overrides, so its filament id lives
  449. on a parent: another of the user's Orca profiles, or a system preset. The
  450. parent's id is what OrcaSlicer resolves the slot to, which beats a generic.
  451. """
  452. ORCA_ID = "34d8f588-860b-5be1-bcbe-c0d46d96324b"
  453. @staticmethod
  454. def _svc(profile, others=()):
  455. svc = MagicMock()
  456. svc.get_profile = AsyncMock(return_value=profile)
  457. svc.list_profiles = AsyncMock(return_value=[profile, *others])
  458. svc.close = AsyncMock()
  459. return svc
  460. async def _lookup(self, svc):
  461. from backend.app.services.slicer_filament_resolver import lookup_orca_filament_id
  462. with patch(
  463. "backend.app.api.routes.orca_cloud._build_authenticated_service",
  464. AsyncMock(return_value=svc),
  465. ):
  466. return await lookup_orca_filament_id(MagicMock(), None, self.ORCA_ID)
  467. @pytest.mark.asyncio
  468. async def test_own_id_needs_no_second_pull(self):
  469. svc = self._svc({"id": self.ORCA_ID, "name": "Mine", "content": {"filament_id": "Pfc74047", "inherits": ""}})
  470. found = await self._lookup(svc)
  471. assert (found.filament_id, found.source, found.reason) == ("Pfc74047", "own", "")
  472. svc.list_profiles.assert_not_awaited()
  473. @pytest.mark.asyncio
  474. async def test_inherited_from_a_user_profile(self):
  475. child = {"id": self.ORCA_ID, "name": "Mine v2", "content": {"inherits": "Mine v1", "filament_type": ["PLA"]}}
  476. parent = {"id": "p", "name": "Mine v1", "content": {"name": "Mine v1", "filament_id": "Pad3f856"}}
  477. found = await self._lookup(self._svc(child, [parent]))
  478. assert (found.filament_id, found.source) == ("Pad3f856", "inherited")
  479. assert found.filament_type == "PLA"
  480. @pytest.mark.asyncio
  481. async def test_through_two_user_profiles_to_a_bambu_system_preset(self):
  482. child = {"id": self.ORCA_ID, "name": "C", "content": {"inherits": "B"}}
  483. middle = {"id": "b", "name": "B", "content": {"inherits": "Bambu PLA Basic @BBL H2D"}}
  484. found = await self._lookup(self._svc(child, [middle]))
  485. # The catalog id OrcaSlicer translates at the printer boundary.
  486. assert (found.filament_id, found.source) == ("GFA00", "inherited")
  487. @pytest.mark.asyncio
  488. async def test_an_unknown_system_parent_leaves_the_fallback(self):
  489. child = {"id": self.ORCA_ID, "name": "C", "content": {"inherits": "Someone PLA @Somewhere"}}
  490. found = await self._lookup(self._svc(child))
  491. assert (found.filament_id, found.reason) == ("", "no_filament_id")
  492. @pytest.mark.asyncio
  493. async def test_an_inherits_cycle_terminates(self):
  494. a = {"id": self.ORCA_ID, "name": "A", "content": {"inherits": "B"}}
  495. b = {"id": "b", "name": "B", "content": {"inherits": "A"}}
  496. found = await self._lookup(self._svc(a, [b]))
  497. assert (found.filament_id, found.reason) == ("", "no_filament_id")
  498. @pytest.mark.asyncio
  499. async def test_a_failed_lookup_says_so(self):
  500. svc = MagicMock()
  501. svc.get_profile = AsyncMock(side_effect=RuntimeError("network down"))
  502. svc.close = AsyncMock()
  503. found = await self._lookup(svc)
  504. assert (found.filament_id, found.reason) == ("", "lookup_failed")
  505. svc.close.assert_awaited()
  506. @pytest.mark.asyncio
  507. async def test_a_caller_without_orca_access_is_told_so(self):
  508. from backend.app.services.slicer_filament_resolver import lookup_orca_filament_id
  509. user = MagicMock()
  510. user.has_permission.return_value = False
  511. found = await lookup_orca_filament_id(MagicMock(), user, self.ORCA_ID)
  512. assert found.reason == "no_permission"
  513. @pytest.mark.asyncio
  514. async def test_a_failed_parent_pull_keeps_name_and_type(self):
  515. """The spool resolver uses them for the slot's brand and type, as it did
  516. before the lookup followed parents."""
  517. child = {"id": self.ORCA_ID, "name": "Mine v2", "content": {"inherits": "Mine v1", "filament_type": ["PETG"]}}
  518. svc = self._svc(child)
  519. svc.list_profiles = AsyncMock(side_effect=RuntimeError("timeout"))
  520. found = await self._lookup(svc)
  521. assert (found.filament_id, found.reason) == ("", "lookup_failed")
  522. assert (found.name, found.filament_type) == ("Mine v2", "PETG")