|
1 | 1 | """DevX: actionable error messages + PlanNotEntitledError specialization. |
2 | 2 |
|
3 | | -A plan-gated 403 should be catchable as PlanNotEntitledError while staying a |
4 | | -ForbiddenError (backward-compatible). Error messages should carry an actionable |
5 | | -hint for auth (401) and plan-gated (403) failures. |
| 3 | +The platform returns plan/entitlement gating as an HTTP 400 with body |
| 4 | +`{status: false, errors: ["… please upgrade to a higher plan …"]}` (see atoms |
| 5 | +main-backend access.middleware.ts). It should surface as PlanNotEntitledError |
| 6 | +while staying a BadRequestError (backward-compatible). |
6 | 7 | """ |
7 | 8 | import unittest |
8 | 9 |
|
9 | 10 | from smallestai import PlanNotEntitledError as PlanNotEntitledFromRoot |
10 | | -from smallestai.atoms.errors.forbidden_error import ForbiddenError, PlanNotEntitledError |
| 11 | +from smallestai.atoms.errors.bad_request_error import BadRequestError, PlanNotEntitledError |
11 | 12 | from smallestai.atoms.errors.unauthorized_error import UnauthorizedError |
12 | 13 | from smallestai.core.api_error import ApiError |
13 | 14 | from smallestai.errors import PlanNotEntitledError as PlanNotEntitledFromErrors |
14 | 15 |
|
| 16 | +_PLAN_GATED_BODY = { |
| 17 | + "status": False, |
| 18 | + "errors": ["Your plan has no access to campaigns, please upgrade to a higher plan to access this feature"], |
| 19 | +} |
| 20 | +_LIMIT_BODY = { |
| 21 | + "status": False, |
| 22 | + "errors": ["You have reached the maximum number of agents for your plan, please upgrade to a higher plan to create more agents"], |
| 23 | +} |
| 24 | + |
15 | 25 |
|
16 | 26 | class ErrorDevxTest(unittest.TestCase): |
17 | | - def test_plan_gated_403_becomes_plan_not_entitled(self): |
18 | | - err = ForbiddenError(body={"message": "DNC is not available on your plan. Upgrade to Enterprise."}) |
| 27 | + def test_plan_gated_400_becomes_plan_not_entitled(self): |
| 28 | + err = BadRequestError(body=_PLAN_GATED_BODY) |
19 | 29 | self.assertIsInstance(err, PlanNotEntitledError) |
20 | | - self.assertIsInstance(err, ForbiddenError) # backward-compatible |
21 | | - self.assertEqual(err.status_code, 403) |
| 30 | + self.assertIsInstance(err, BadRequestError) |
| 31 | + self.assertEqual(err.status_code, 400) |
| 32 | + |
| 33 | + def test_plan_limit_400_also_specializes(self): |
| 34 | + self.assertIsInstance(BadRequestError(body=_LIMIT_BODY), PlanNotEntitledError) |
22 | 35 |
|
23 | | - def test_generic_403_stays_forbidden(self): |
24 | | - err = ForbiddenError(body={"message": "You do not have access to this resource."}) |
25 | | - self.assertIsInstance(err, ForbiddenError) |
| 36 | + def test_generic_400_stays_bad_request(self): |
| 37 | + err = BadRequestError(body={"status": False, "errors": ["voiceId is required"]}) |
| 38 | + self.assertIsInstance(err, BadRequestError) |
26 | 39 | self.assertNotIsInstance(err, PlanNotEntitledError) |
27 | 40 |
|
28 | | - def test_explicit_plan_error_construction(self): |
29 | | - err = PlanNotEntitledError(body={"message": "feature_not_entitled"}) |
30 | | - self.assertIsInstance(err, PlanNotEntitledError) |
31 | | - self.assertEqual(err.status_code, 403) |
| 41 | + def test_catch_as_plan_not_entitled(self): |
| 42 | + try: |
| 43 | + raise BadRequestError(body=_PLAN_GATED_BODY) |
| 44 | + except PlanNotEntitledError: |
| 45 | + pass |
| 46 | + else: |
| 47 | + self.fail("expected PlanNotEntitledError") |
32 | 48 |
|
33 | 49 | def test_exported_symbols_are_the_same_class(self): |
34 | 50 | self.assertIs(PlanNotEntitledFromRoot, PlanNotEntitledError) |
35 | 51 | self.assertIs(PlanNotEntitledFromErrors, PlanNotEntitledError) |
36 | 52 |
|
37 | 53 | def test_401_message_has_auth_hint(self): |
38 | | - msg = str(UnauthorizedError(body={"message": "invalid token"})) |
39 | | - self.assertIn("SMALLEST_API_KEY", msg) |
| 54 | + self.assertIn("SMALLEST_API_KEY", str(UnauthorizedError(body={"errors": ["invalid token"]}))) |
40 | 55 |
|
41 | 56 | def test_plan_gated_message_has_upgrade_hint(self): |
42 | | - msg = str(ForbiddenError(body={"message": "requires a paid plan; upgrade your plan"})) |
43 | | - self.assertIn("plan-gated", msg.lower()) |
| 57 | + msg = str(BadRequestError(body=_PLAN_GATED_BODY)).lower() |
| 58 | + self.assertIn("plan", msg) |
| 59 | + self.assertIn("upgrade", msg) |
44 | 60 |
|
45 | | - def test_success_case_no_hint_noise(self): |
46 | | - # A non-auth, non-403 error keeps a clean message. |
| 61 | + def test_no_hint_noise_on_plain_error(self): |
47 | 62 | msg = str(ApiError(status_code=500, body="boom")) |
48 | 63 | self.assertIn("boom", msg) |
49 | 64 | self.assertNotIn("SMALLEST_API_KEY", msg) |
|
0 commit comments