listingsapi 0.5.2__tar.gz → 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {listingsapi-0.5.2/src/listingsapi.egg-info → listingsapi-0.5.4}/PKG-INFO +3 -3
  2. {listingsapi-0.5.2 → listingsapi-0.5.4}/README.md +2 -2
  3. {listingsapi-0.5.2 → listingsapi-0.5.4}/pyproject.toml +1 -1
  4. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/__init__.py +1 -1
  5. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/_client.py +5 -1
  6. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/connected_accounts.py +1 -1
  7. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/locations.py +4 -2
  8. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/photos.py +1 -1
  9. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/posts.py +20 -14
  10. {listingsapi-0.5.2 → listingsapi-0.5.4/src/listingsapi.egg-info}/PKG-INFO +3 -3
  11. {listingsapi-0.5.2 → listingsapi-0.5.4}/tests/test_listingsapi.py +88 -13
  12. {listingsapi-0.5.2 → listingsapi-0.5.4}/LICENSE +0 -0
  13. {listingsapi-0.5.2 → listingsapi-0.5.4}/setup.cfg +0 -0
  14. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/_types.py +0 -0
  15. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/_utils.py +0 -0
  16. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/exceptions.py +0 -0
  17. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/py.typed +0 -0
  18. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/__init__.py +0 -0
  19. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/_base.py +0 -0
  20. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/analytics.py +0 -0
  21. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/listings.py +0 -0
  22. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/reviews.py +0 -0
  23. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi/resources/workflows.py +0 -0
  24. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi.egg-info/SOURCES.txt +0 -0
  25. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi.egg-info/dependency_links.txt +0 -0
  26. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi.egg-info/requires.txt +0 -0
  27. {listingsapi-0.5.2 → listingsapi-0.5.4}/src/listingsapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: listingsapi
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing
5
5
  Author-email: listingsAPI <support@listingsapi.com>
6
6
  License: MIT License
@@ -355,7 +355,7 @@ client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")
355
355
 
356
356
  ```python
357
357
  sites = client.plan_sites() # directories included in your plan
358
- countries = client.countries() # supported countries and states
358
+ countries = client.countries() # supported countries, states, and address rules
359
359
  subcategories = client.subcategories() # business categories (IDs for locations.add)
360
360
  subs = client.subscriptions() # active subscriptions
361
361
  ```
@@ -389,7 +389,7 @@ print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")
389
389
  | Method | Description |
390
390
  |--------|-------------|
391
391
  | `client.plan_sites()` | Directories included in your plan |
392
- | `client.countries()` | Supported countries and states (ISO codes) |
392
+ | `client.countries()` | Supported countries and states (ISO codes), with `hasCity`, `stateRequired` and `hasPostalCode` for each country |
393
393
  | `client.subcategories()` | Business categories (IDs for `locations.add`) |
394
394
  | `client.subscriptions()` | Active subscriptions for the account |
395
395
 
@@ -303,7 +303,7 @@ client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")
303
303
 
304
304
  ```python
305
305
  sites = client.plan_sites() # directories included in your plan
306
- countries = client.countries() # supported countries and states
306
+ countries = client.countries() # supported countries, states, and address rules
307
307
  subcategories = client.subcategories() # business categories (IDs for locations.add)
308
308
  subs = client.subscriptions() # active subscriptions
309
309
  ```
@@ -337,7 +337,7 @@ print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")
337
337
  | Method | Description |
338
338
  |--------|-------------|
339
339
  | `client.plan_sites()` | Directories included in your plan |
340
- | `client.countries()` | Supported countries and states (ISO codes) |
340
+ | `client.countries()` | Supported countries and states (ISO codes), with `hasCity`, `stateRequired` and `hasPostalCode` for each country |
341
341
  | `client.subcategories()` | Business categories (IDs for `locations.add`) |
342
342
  | `client.subscriptions()` | Active subscriptions for the account |
343
343
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "listingsapi"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -23,7 +23,7 @@ from listingsapi.exceptions import (
23
23
  ValidationError,
24
24
  )
25
25
 
26
- __version__ = "0.5.2"
26
+ __version__ = "0.5.4"
27
27
 
28
28
  __all__ = [
29
29
  # Client
@@ -263,7 +263,11 @@ class ListingsAPI:
263
263
  return [APIObject(item) for item in items]
264
264
 
265
265
  def countries(self) -> list[APIObject]:
266
- """Get supported countries and states (ISO codes)."""
266
+ """Get supported countries and states (ISO codes).
267
+
268
+ Each country has ``hasCity``, ``stateRequired`` and ``hasPostalCode``,
269
+ which tell you which address fields a location in that country needs.
270
+ """
267
271
  data = self._get("countries")
268
272
  items = data.get("data", {}).get("supportedCountries") or []
269
273
  return [APIObject(item) for item in items]
@@ -147,7 +147,7 @@ class ConnectedAccounts(APIResource):
147
147
  }
148
148
  },
149
149
  )
150
- return APIObject(data.get("data", {}).get("connectUrl") or {})
150
+ return APIObject(data.get("data", {}).get("createConnectUrl") or {})
151
151
 
152
152
  def oauth_disconnect(self, location_id: str | int, site: str) -> APIObject:
153
153
  """Disconnect a Google/Facebook profile from a single location."""
@@ -191,8 +191,10 @@ class Locations(APIResource):
191
191
  city: City. Required for countries with city-level addressing; may be
192
192
  omitted only for service-area businesses with hide_address=True.
193
193
  street: Street address.
194
- state_iso: State/region ISO code.
195
- postal_code: Postal or ZIP code.
194
+ state_iso: State/region ISO code. Required when the country's
195
+ ``stateRequired`` is true (see ``client.countries()``).
196
+ postal_code: Postal or ZIP code. Send it when the country's
197
+ ``hasPostalCode`` is true.
196
198
  phone: Primary phone number, validated for the country.
197
199
  website: Business website URL.
198
200
  store_id: Your unique store code; must be unique across the account.
@@ -44,7 +44,7 @@ class Photos(APIResource):
44
44
  """Star or unstar photos. Max 4 starred photos per account."""
45
45
  data = self._post(
46
46
  "locations/photos/star",
47
- {"input": {"locationId": encode_location_id(location_id), "mediaIds": media_ids, "starred": starred}},
47
+ {"input": {"locationId": encode_location_id(location_id), "photoIds": media_ids, "starred": starred}},
48
48
  )
49
49
  return APIObject(data.get("data", {}).get("starUnstarLocationPhotos") or {})
50
50
 
@@ -30,13 +30,14 @@ class Posts(APIResource):
30
30
  """
31
31
 
32
32
  def create(self, body: dict[str, Any]) -> APIObject:
33
- """Create a post from a raw body dict (flat fields, not input-wrapped).
33
+ """Create a post from a raw field dict.
34
34
 
35
- Required: postName, locationIds (base64), postType, postSites. Prefer
36
- create_announcement / create_event / create_offer for keyword arguments
37
- and client-side validation.
35
+ Pass the fields flat — they are wrapped in the ``input`` object the API
36
+ requires before sending. Required: postName, locationIds (base64),
37
+ postType, postSites. Prefer create_announcement / create_event /
38
+ create_offer for keyword arguments and client-side validation.
38
39
  """
39
- data = self._post("posts", body)
40
+ data = self._post("posts", {"input": body})
40
41
  return APIObject(data.get("data", {}).get("createSocialPost") or {})
41
42
 
42
43
  def create_announcement(
@@ -232,8 +233,8 @@ class Posts(APIResource):
232
233
  context_info=context_info,
233
234
  additional_fields=additional_fields,
234
235
  )
235
- data = self._post("bulk-posts", body)
236
- return APIObject(data.get("data", {}).get("createBulkSocialPost") or {})
236
+ data = self._post("bulk-posts", {"input": body})
237
+ return APIObject(data.get("data", {}).get("createSocialPost") or {})
237
238
 
238
239
  def retrieve(self, post_id: str) -> APIObject:
239
240
  """Get a post with its content, per-site publish status, and analytics."""
@@ -258,13 +259,16 @@ class Posts(APIResource):
258
259
  """List post campaigns targeting a location.
259
260
 
260
261
  Offset-paginated: returns .records plus .pageInfo with totalPages,
261
- totalRecords, hasNextPage. tag defaults to "all" (the API errors when
262
- it is omitted).
262
+ totalRecords, hasNextPage.
263
+
264
+ Note: this route rejects a tag — the API errors when one is sent — so
265
+ ``tag`` is accepted for backwards compatibility and never put on the
266
+ wire. Use bulk_list_for_location when you need tag filtering.
263
267
  """
264
- params = self._list_params(tag, page, per_page, filters, sort_fields)
268
+ params = self._list_params(None, page, per_page, filters, sort_fields)
265
269
  encoded = encode_location_id(location_id)
266
270
  data = self._get(f"locations/{encoded}/posts", params)
267
- return APIObject(data.get("data", {}).get("rollupSocialPosts") or {})
271
+ return APIObject(data.get("data", {}).get("postsByLocation") or {})
268
272
 
269
273
  def bulk_retrieve(self, bulk_post_id: str) -> APIObject:
270
274
  """Get a bulk campaign with per-location publish status and analytics."""
@@ -293,7 +297,7 @@ class Posts(APIResource):
293
297
  sites=sites if sites is not None else ["GOOGLE"],
294
298
  **kwargs,
295
299
  )
296
- data = self._post("posts", body)
300
+ data = self._post("posts", {"input": body})
297
301
  return APIObject(data.get("data", {}).get("createSocialPost") or {})
298
302
 
299
303
  def _build_post_body(
@@ -358,13 +362,15 @@ class Posts(APIResource):
358
362
 
359
363
  def _list_params(
360
364
  self,
361
- tag: str,
365
+ tag: str | None,
362
366
  page: int | None,
363
367
  per_page: int | None,
364
368
  filters: dict[str, Any] | None,
365
369
  sort_fields: dict[str, Any] | None,
366
370
  ) -> dict[str, Any]:
367
- params: dict[str, Any] = {"tag": tag}
371
+ params: dict[str, Any] = {}
372
+ if tag is not None:
373
+ params["tag"] = tag
368
374
  if page is not None:
369
375
  params["page"] = page
370
376
  if per_page is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: listingsapi
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing
5
5
  Author-email: listingsAPI <support@listingsapi.com>
6
6
  License: MIT License
@@ -355,7 +355,7 @@ client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")
355
355
 
356
356
  ```python
357
357
  sites = client.plan_sites() # directories included in your plan
358
- countries = client.countries() # supported countries and states
358
+ countries = client.countries() # supported countries, states, and address rules
359
359
  subcategories = client.subcategories() # business categories (IDs for locations.add)
360
360
  subs = client.subscriptions() # active subscriptions
361
361
  ```
@@ -389,7 +389,7 @@ print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")
389
389
  | Method | Description |
390
390
  |--------|-------------|
391
391
  | `client.plan_sites()` | Directories included in your plan |
392
- | `client.countries()` | Supported countries and states (ISO codes) |
392
+ | `client.countries()` | Supported countries and states (ISO codes), with `hasCity`, `stateRequired` and `hasPostalCode` for each country |
393
393
  | `client.subcategories()` | Business categories (IDs for `locations.add`) |
394
394
  | `client.subscriptions()` | Active subscriptions for the account |
395
395
 
@@ -450,7 +450,7 @@ def test_analytics_google(client):
450
450
 
451
451
 
452
452
  def test_version():
453
- assert listingsapi.__version__ == "0.5.2"
453
+ assert listingsapi.__version__ == "0.5.4"
454
454
 
455
455
 
456
456
  # --- locations.add (one-call create with validation) ---
@@ -658,7 +658,7 @@ CREATE_POST_OK = {
658
658
  }
659
659
  BULK_POST_OK = {
660
660
  "data": {
661
- "createBulkSocialPost": {
661
+ "createSocialPost": {
662
662
  "success": True,
663
663
  "errors": [],
664
664
  "socialPost": {"id": "QnVsa1Bvc3Q6OTk=", "status": "INPROGRESS"},
@@ -667,7 +667,7 @@ BULK_POST_OK = {
667
667
  }
668
668
 
669
669
 
670
- def test_posts_create_announcement_builds_flat_body(client):
670
+ def test_posts_create_announcement_wraps_body_in_input(client):
671
671
  with requests_mock.Mocker() as m:
672
672
  m.post("https://listingsapi.com/api/v4/posts", json=CREATE_POST_OK)
673
673
  result = client.posts.create_announcement(
@@ -680,8 +680,9 @@ def test_posts_create_announcement_builds_flat_body(client):
680
680
  media_url="https://cdn.example.com/opening.jpg",
681
681
  )
682
682
  assert result.success is True
683
- sent = m.last_request.json()
684
- assert "input" not in sent
683
+ envelope = m.last_request.json()
684
+ assert list(envelope) == ["input"]
685
+ sent = envelope["input"]
685
686
  assert sent["postName"] == "Grand Opening"
686
687
  assert sent["postType"] == "ANNOUNCEMENT"
687
688
  assert sent["postSites"] == ["GOOGLE"]
@@ -691,6 +692,34 @@ def test_posts_create_announcement_builds_flat_body(client):
691
692
  assert sent["postMediaUrl"] == [{"site": "GOOGLE", "url": "https://cdn.example.com/opening.jpg", "type": "IMAGE"}]
692
693
 
693
694
 
695
+ def test_posts_create_raw_body_is_input_wrapped(client):
696
+ with requests_mock.Mocker() as m:
697
+ m.post("https://listingsapi.com/api/v4/posts", json=CREATE_POST_OK)
698
+ result = client.posts.create({
699
+ "postName": "Raw",
700
+ "locationIds": ["TG9jYXRpb246MTY4MDg="],
701
+ "postType": "ANNOUNCEMENT",
702
+ "postSites": ["GOOGLE"],
703
+ })
704
+ assert result.success is True
705
+ envelope = m.last_request.json()
706
+ assert list(envelope) == ["input"]
707
+ assert envelope["input"]["postName"] == "Raw"
708
+
709
+
710
+ def test_posts_bulk_publish_wraps_body_in_input(client):
711
+ with requests_mock.Mocker() as m:
712
+ m.post("https://listingsapi.com/api/v4/bulk-posts", json=BULK_POST_OK)
713
+ result = client.posts.bulk_publish(
714
+ name="Holiday hours", location_ids=[16808], message="Open late!",
715
+ )
716
+ # reads createSocialPost, not createBulkSocialPost
717
+ assert result.socialPost.id == "QnVsa1Bvc3Q6OTk="
718
+ envelope = m.last_request.json()
719
+ assert list(envelope) == ["input"]
720
+ assert envelope["input"]["postName"] == "Holiday hours"
721
+
722
+
694
723
  def test_posts_bulk_publish_defaults_to_google_and_facebook(client):
695
724
  with requests_mock.Mocker() as m:
696
725
  m.post("https://listingsapi.com/api/v4/bulk-posts", json=BULK_POST_OK)
@@ -700,7 +729,7 @@ def test_posts_bulk_publish_defaults_to_google_and_facebook(client):
700
729
  message="Open late through the holidays!",
701
730
  )
702
731
  assert result.success is True
703
- sent = m.last_request.json()
732
+ sent = m.last_request.json()["input"]
704
733
  assert sent["postSites"] == ["GOOGLE", "FACEBOOK"]
705
734
  assert len(sent["postMessage"]) == 2
706
735
  assert {e["site"] for e in sent["postMessage"]} == {"GOOGLE", "FACEBOOK"}
@@ -715,7 +744,7 @@ def test_posts_bulk_publish_per_site_messages(client):
715
744
  location_ids=[16808],
716
745
  message={"GOOGLE": "New summer menu!", "FACEBOOK": "Swing by for the summer menu."},
717
746
  )
718
- sent = m.last_request.json()
747
+ sent = m.last_request.json()["input"]
719
748
  by_site = {e["site"]: e["message"] for e in sent["postMessage"]}
720
749
  assert by_site["GOOGLE"] == "New summer menu!"
721
750
  assert by_site["FACEBOOK"] == "Swing by for the summer menu."
@@ -767,7 +796,7 @@ def test_posts_create_event_context(client):
767
796
  start_time="7:00pm",
768
797
  end_time="10:00pm",
769
798
  )
770
- sent = m.last_request.json()
799
+ sent = m.last_request.json()["input"]
771
800
  assert sent["postType"] == "EVENT"
772
801
  assert sent["postContextInfo"] == {
773
802
  "title": "Jazz Night", "startDay": "2026-08-01", "endDay": "2026-08-01",
@@ -789,7 +818,7 @@ def test_posts_create_offer_context(client):
789
818
  start_day="2026-08-01",
790
819
  end_day="2026-08-07",
791
820
  )
792
- sent = m.last_request.json()
821
+ sent = m.last_request.json()["input"]
793
822
  assert sent["postType"] == "OFFER"
794
823
  assert sent["postContextInfo"]["couponCode"] == "SUMMER20"
795
824
  assert sent["postContextInfo"]["title"] == "Summer Sale"
@@ -808,17 +837,35 @@ def test_posts_retrieve_and_bulk_retrieve(client):
808
837
  assert bulk.socialPostId == "QnVsa1Bvc3Q6OTk="
809
838
 
810
839
 
811
- def test_posts_list_for_location_defaults_tag_all(client):
812
- response = {"data": {"rollupSocialPosts": {"records": [{"id": "p1"}], "pageInfo": {"totalPages": 1, "totalRecords": 1, "hasNextPage": False}}}}
840
+ def test_posts_list_for_location_sends_no_tag_and_reads_posts_by_location(client):
841
+ response = {"data": {"postsByLocation": {"records": [{"id": "p1"}], "pageInfo": {"totalPages": 1, "totalRecords": 1, "hasNextPage": False}}}}
813
842
  with requests_mock.Mocker() as m:
814
843
  m.get("https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTY4MDg=/posts", json=response)
815
844
  result = client.posts.list_for_location(16808, page=1, per_page=10)
816
845
  assert result.records[0].id == "p1"
817
846
  assert result.pageInfo.totalRecords == 1
818
- assert m.last_request.qs["tag"] == ["all"]
847
+ # the route raises when a tag is supplied, so none is sent
848
+ assert "tag" not in m.last_request.qs
819
849
  assert m.last_request.qs["page"] == ["1"]
820
850
 
821
851
 
852
+ def test_posts_list_for_location_ignores_tag_argument(client):
853
+ response = {"data": {"postsByLocation": {"records": []}}}
854
+ with requests_mock.Mocker() as m:
855
+ m.get("https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTY4MDg=/posts", json=response)
856
+ client.posts.list_for_location(16808, tag="all")
857
+ assert "tag" not in m.last_request.qs
858
+
859
+
860
+ def test_posts_bulk_list_for_location_still_sends_tag(client):
861
+ response = {"data": {"rollupSocialPosts": {"records": [{"id": "b1"}]}}}
862
+ with requests_mock.Mocker() as m:
863
+ m.get("https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTY4MDg=/bulk-posts", json=response)
864
+ result = client.posts.bulk_list_for_location(16808)
865
+ assert result.records[0].id == "b1"
866
+ assert m.last_request.qs["tag"] == ["all"]
867
+
868
+
822
869
  def test_posts_delete(client):
823
870
  with requests_mock.Mocker() as m:
824
871
  m.delete("https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg==",
@@ -828,7 +875,7 @@ def test_posts_delete(client):
828
875
 
829
876
 
830
877
  def test_posts_bulk_publish_api_failure_raises(client):
831
- body = {"data": {"createBulkSocialPost": {"success": False, "errors": [{"code": "SY20001", "message": "Image URL unreachable"}]}}}
878
+ body = {"data": {"createSocialPost": {"success": False, "errors": [{"code": "SY20001", "message": "Image URL unreachable"}]}}}
832
879
  with requests_mock.Mocker() as m:
833
880
  m.post("https://listingsapi.com/api/v4/bulk-posts", json=body)
834
881
  with pytest.raises(ValidationError) as exc:
@@ -955,3 +1002,31 @@ def test_post_without_key_sends_no_idempotency_header(client):
955
1002
  m.post("https://listingsapi.com/api/v4/locations", json={"data": {}})
956
1003
  client._post("locations", {"input": {"name": "Acme"}})
957
1004
  assert "Idempotency-Key" not in m.last_request.headers
1005
+
1006
+
1007
+ # --- Wire-shape fixes swept in 0.5.3 ---
1008
+
1009
+ def test_photos_star_sends_photo_ids(client):
1010
+ response = {"data": {"starUnstarLocationPhotos": {"success": True}}}
1011
+ with requests_mock.Mocker() as m:
1012
+ m.post("https://listingsapi.com/api/v4/locations/photos/star", json=response)
1013
+ result = client.photos.star(16808, ["TWVkaWFGaWxlOjE="])
1014
+ assert result.success is True
1015
+ sent = m.last_request.json()["input"]
1016
+ assert sent["photoIds"] == ["TWVkaWFGaWxlOjE="]
1017
+ assert "mediaIds" not in sent
1018
+ assert sent["starred"] is True
1019
+ assert sent["locationId"] == "TG9jYXRpb246MTY4MDg="
1020
+
1021
+
1022
+ def test_connected_accounts_oauth_url_reads_create_connect_url(client):
1023
+ response = {"data": {"createConnectUrl": {"url": "https://oauth.example/x", "success": True}}}
1024
+ with requests_mock.Mocker() as m:
1025
+ m.post("https://listingsapi.com/api/v4/locations/oauth_connect_url", json=response)
1026
+ result = client.connected_accounts.oauth_url(
1027
+ 16808, "google", "https://ok.example", "https://err.example"
1028
+ )
1029
+ assert result.url == "https://oauth.example/x"
1030
+ sent = m.last_request.json()["input"]
1031
+ assert sent["locationId"] == "TG9jYXRpb246MTY4MDg="
1032
+ assert sent["site"] == "GOOGLE"
File without changes
File without changes