|
| 1 | +""" |
| 2 | +Tests for BulkListUsers — the batch lookup used to reconcile a migration. |
| 3 | +
|
| 4 | +Verifies the request shape the SDK sends, that lookups by `custom` resolve back to |
| 5 | +the tokens returned at creation time, and that the documented `limit` default of 10 |
| 6 | +is not applied to this endpoint. |
| 7 | +""" |
| 8 | + |
| 9 | +import os |
| 10 | +import random |
| 11 | +import unittest |
| 12 | + |
| 13 | +import requests |
| 14 | + |
| 15 | +from databunkerpro import DatabunkerproAPI |
| 16 | + |
| 17 | +# Deliberately more than the `limit: 10` default documented for this endpoint. |
| 18 | +USER_COUNT = 12 |
| 19 | + |
| 20 | + |
| 21 | +class TestBulkListUsers(unittest.TestCase): |
| 22 | + """BulkListUsers batch lookup by indexed field.""" |
| 23 | + |
| 24 | + @classmethod |
| 25 | + def setUpClass(cls): |
| 26 | + cls.api_url = os.getenv("DATABUNKER_API_URL", "https://pro.databunker.org") |
| 27 | + cls.api_token = os.getenv("DATABUNKER_API_TOKEN", "") |
| 28 | + cls.tenant_name = os.getenv("DATABUNKER_TENANT_NAME", "") |
| 29 | + |
| 30 | + if not all([cls.api_token, cls.tenant_name]): |
| 31 | + try: |
| 32 | + response = requests.get( |
| 33 | + "https://databunker.org/api/newtenant.php", verify=False |
| 34 | + ) |
| 35 | + data = response.json() if response.ok else None |
| 36 | + if not data or data.get("status") != "ok": |
| 37 | + raise unittest.SkipTest("Failed to get credentials from sandbox") |
| 38 | + cls.tenant_name = data["tenantname"] |
| 39 | + cls.api_token = data["xtoken"] |
| 40 | + print(f"\nSandbox tenant: {cls.tenant_name} at {cls.api_url}") |
| 41 | + except unittest.SkipTest: |
| 42 | + raise |
| 43 | + except Exception as e: |
| 44 | + raise unittest.SkipTest(f"Failed to get credentials: {e}") |
| 45 | + |
| 46 | + cls.api = DatabunkerproAPI(cls.api_url, cls.api_token, cls.tenant_name) |
| 47 | + |
| 48 | + # Create users the way a migration does: the legacy primary key in `custom`. |
| 49 | + cls.run_id = random.randint(100000, 999999) |
| 50 | + records = [ |
| 51 | + { |
| 52 | + "profile": { |
| 53 | + "custom": f"legacy-{cls.run_id}-{i}", |
| 54 | + "email": f"blu{cls.run_id}x{i}@example.com", |
| 55 | + "name": f"Bulk List User {i}", |
| 56 | + } |
| 57 | + } |
| 58 | + for i in range(USER_COUNT) |
| 59 | + ] |
| 60 | + result = cls.api.create_users_bulk(records) |
| 61 | + if result.get("status") != "ok": |
| 62 | + raise unittest.SkipTest(f"Setup bulk create failed: {result}") |
| 63 | + |
| 64 | + # custom value -> token, as returned by UserCreateBulk |
| 65 | + cls.expected = { |
| 66 | + item["profile"]["custom"]: item["token"] for item in result["created"] |
| 67 | + } |
| 68 | + print(f"Created {len(cls.expected)}/{USER_COUNT} users for lookup") |
| 69 | + |
| 70 | + def _unlock(self): |
| 71 | + result = self.api.bulk_list_unlock() |
| 72 | + self.assertEqual(result.get("status"), "ok", f"unlock failed: {result}") |
| 73 | + self.assertIn("unlockuuid", result) |
| 74 | + return result["unlockuuid"] |
| 75 | + |
| 76 | + def _list_users(self, criteria, unlock_uuid=None): |
| 77 | + """BulkListUsers, skipping when the server has the feature turned off. |
| 78 | +
|
| 79 | + BulkListUsers is gated by the `list_users` configuration flag, which is off by |
| 80 | + default. Skipping rather than failing keeps a server-side setting from breaking |
| 81 | + the build — and, since `deploy` needs `test`, from blocking a release. |
| 82 | + """ |
| 83 | + if unlock_uuid is None: |
| 84 | + unlock_uuid = self._unlock() |
| 85 | + result = self.api.bulk_list_users(unlock_uuid, criteria) |
| 86 | + if result.get("message") == "BulkListUsers is disabled": |
| 87 | + self.skipTest("list_users is not enabled on this server") |
| 88 | + return result |
| 89 | + |
| 90 | + def test_bulk_list_unlock(self): |
| 91 | + """BulkListUnlock issues a UUID.""" |
| 92 | + self.assertTrue(self._unlock()) |
| 93 | + |
| 94 | + def test_bulk_list_users_by_custom(self): |
| 95 | + """Every created user is found by its `custom` value, with a matching token.""" |
| 96 | + criteria = [{"mode": "custom", "identity": c} for c in self.expected] |
| 97 | + result = self._list_users(criteria) |
| 98 | + |
| 99 | + self.assertEqual(result.get("status"), "ok", f"unexpected response: {result}") |
| 100 | + rows = result.get("rows") or [] |
| 101 | + self.assertTrue(rows, f"no rows returned: {result}") |
| 102 | + |
| 103 | + found = { |
| 104 | + row["profile"]["custom"]: row["token"] |
| 105 | + for row in rows |
| 106 | + if row.get("profile", {}).get("custom") |
| 107 | + } |
| 108 | + self.assertEqual( |
| 109 | + found, |
| 110 | + self.expected, |
| 111 | + "token mapping from BulkListUsers does not match UserCreateBulk", |
| 112 | + ) |
| 113 | + |
| 114 | + def test_limit_default_is_not_applied(self): |
| 115 | + """More than 10 criteria must all come back — `limit` is not honoured here.""" |
| 116 | + criteria = [{"mode": "custom", "identity": c} for c in self.expected] |
| 117 | + self.assertGreater(len(criteria), 10, "test needs >10 records to be meaningful") |
| 118 | + |
| 119 | + result = self._list_users(criteria) |
| 120 | + rows = result.get("rows") or [] |
| 121 | + |
| 122 | + self.assertEqual( |
| 123 | + len(rows), |
| 124 | + len(self.expected), |
| 125 | + f"expected {len(self.expected)} rows, got {len(rows)} — " |
| 126 | + f"limit appears to be applied (total={result.get('total')})", |
| 127 | + ) |
| 128 | + |
| 129 | + def test_unknown_identity_returns_no_row(self): |
| 130 | + """A `custom` value that was never stored yields nothing, not an error.""" |
| 131 | + result = self._list_users( |
| 132 | + [{"mode": "custom", "identity": f"legacy-{self.run_id}-absent"}] |
| 133 | + ) |
| 134 | + self.assertEqual(result.get("status"), "ok", f"unexpected response: {result}") |
| 135 | + self.assertEqual(len(result.get("rows") or []), 0) |
| 136 | + |
| 137 | + def test_invalid_unlockuuid_is_rejected(self): |
| 138 | + """unlockuuid is enforced — a bogus UUID must not return data.""" |
| 139 | + result = self._list_users( |
| 140 | + [{"mode": "custom", "identity": next(iter(self.expected))}], |
| 141 | + unlock_uuid="00000000-0000-0000-0000-000000000000", |
| 142 | + ) |
| 143 | + self.assertNotEqual( |
| 144 | + result.get("status"), |
| 145 | + "ok", |
| 146 | + f"a bogus unlockuuid returned data: {result}", |
| 147 | + ) |
| 148 | + |
| 149 | + |
| 150 | +if __name__ == "__main__": |
| 151 | + unittest.main() |
0 commit comments