← back to Shopify Room Mockup

tests/test_ledger_guard.py

75 lines

"""Negative test for the TK-12096 double-run guard: a second run over products the
ledger already holds must be a no-op (no render, no upload); --force and a
ledgered deletion re-open it. No network, no Shopify: select/render/upload stubbed."""
import importlib.util, io, json, os, sys, tempfile, unittest
from contextlib import redirect_stdout

HERE = os.path.dirname(os.path.abspath(__file__))
spec = importlib.util.spec_from_file_location('srm', os.path.join(HERE, '..', 'shopify-room-mockup.py'))
srm = importlib.util.module_from_spec(spec); spec.loader.exec_module(srm)

GIDS = ['gid://shopify/Product/1', 'gid://shopify/Product/2']


class Guard(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.mkdtemp()
        self.ledger = os.path.join(self.tmp, 'ledger.jsonl')
        self.uploads = []
        srm.load_token = lambda env: 'x'
        srm.select_products = lambda sh, a: [
            {'id': g, 'title': g, 'image_url': 'http://img', 'has_room': False} for g in GIDS]
        srm.render_room = lambda *a, **k: b'png'
        n = iter(range(100, 200))
        def fake_upload(sh, pid, buf, alt):
            self.uploads.append(pid)
            return True, [{'id': f'gid://shopify/MediaImage/{next(n)}'}]
        srm.upload_media = fake_upload

    def run_tool(self, *extra):
        argv = ['x', '--ids', '1,2', '--ledger', self.ledger, '--ticket', 'TK-T', *extra]
        old, sys.argv = sys.argv, argv
        out = io.StringIO()
        try:
            with redirect_stdout(out):
                srm.main()
        finally:
            sys.argv = old
        return out.getvalue()

    def test_second_run_is_noop(self):
        self.run_tool()
        self.assertEqual(self.uploads, GIDS)
        out = self.run_tool()
        self.assertEqual(self.uploads, GIDS, 'second run must not upload again')
        self.assertIn('added=0 skipped=2', out)

    def test_force_overrides(self):
        self.run_tool()
        self.run_tool('--force')
        self.assertEqual(len(self.uploads), 4)

    def test_other_ticket_not_blocked(self):
        self.run_tool()
        self.run_tool('--ticket', 'TK-OTHER')
        self.assertEqual(len(self.uploads), 4)

    def test_deleted_row_reopens_product(self):
        self.run_tool()
        with open(self.ledger) as fh: rows = [json.loads(l) for l in fh]
        with open(self.ledger, 'a') as f:
            f.write(json.dumps({'ticket': 'TK-T', 'action': 'deleted',
                                'product_gid': rows[0]['product_gid'], 'media_id': rows[0]['media_id']}) + '\n')
        self.run_tool()
        self.assertEqual(self.uploads, GIDS + [GIDS[0]])

    def test_corrupt_ledger_fails_closed(self):
        open(self.ledger, 'w').write('{not json\n')
        with self.assertRaises(Exception):
            self.run_tool()
        self.assertEqual(self.uploads, [])


if __name__ == '__main__':
    unittest.main()