@@ -1794,3 +1794,120 @@ async def test_item_asset_change(app_client, load_test_data):
17941794 # We get "𒍟※" because PgSTAC set it when ingesting (`description`is item-assets)
17951795 # because we removed item-assets, pgstac cannot hydrate this field, and thus return "𒍟※"
17961796 assert resp .json ()["features" ][0 ]["assets" ]["qa_pixel" ]["description" ] == "𒍟※"
1797+
1798+ @pytest .mark .parametrize ("usecase" , ("ok1" , "ok2" , "ko" ))
1799+ async def test_pagination_different_collections (app_client , load_test_data , usecase ):
1800+ """
1801+ This test demonstrates an error case ("ko" use case): when the same item exists in two collections, the
1802+ /search endpoint without 'limit' parameter returns the expected result = all items from all collections.
1803+ But when we add a 'limit' parameter, the /search endpoint fails: it returns only items from the first collection,
1804+ with a 'next' link that should allow to get items from the next collections, but this 'next' link doesn't work:
1805+ it returns nothing.
1806+
1807+ This test also implements two nominal use cases that work as expected ("ok1" and "ok2"): when two different items
1808+ exist in the same collection, and when two different items exist in two different collections.
1809+ """
1810+
1811+ col1_data = load_test_data ("test_collection.json" )
1812+ col2_data = load_test_data ("test2_collection.json" )
1813+
1814+ item1_data = load_test_data ("test_item.json" )
1815+ item2_data = load_test_data ("test_item2.json" )
1816+
1817+ # KO use case: the same item exists in two different collections
1818+ if usecase == "ko" :
1819+ assert (await app_client .post ("/collections" , json = col1_data )).status_code == 201
1820+ assert (await app_client .post ("/collections" , json = col2_data )).status_code == 201
1821+
1822+ for col_data in col1_data , col2_data :
1823+ item1_data ["collection" ] = col_data ["id" ]
1824+ assert (await app_client .post (f"/collections/{ col_data ['id' ]} /items" , json = item1_data )).status_code == 201
1825+
1826+ # First OK use case: two different items in the same collection
1827+ elif usecase == "ok1" :
1828+ assert (await app_client .post ("/collections" , json = col1_data )).status_code == 201
1829+
1830+ for item_data in item1_data , item2_data :
1831+ item_data ["collection" ] = col1_data ["id" ]
1832+ assert (await app_client .post (f"/collections/{ col1_data ['id' ]} /items" , json = item_data )).status_code == 201
1833+
1834+ # Second OK use case: two different items in two different collections
1835+ elif usecase == "ok2" :
1836+ assert (await app_client .post ("/collections" , json = col1_data )).status_code == 201
1837+ assert (await app_client .post ("/collections" , json = col2_data )).status_code == 201
1838+
1839+ item1_data ["collection" ] = col1_data ["id" ]
1840+ assert (await app_client .post (f"/collections/{ col1_data ['id' ]} /items" , json = item1_data )).status_code == 201
1841+
1842+ item2_data ["collection" ] = col2_data ["id" ]
1843+ assert (await app_client .post (f"/collections/{ col2_data ['id' ]} /items" , json = item2_data )).status_code == 201
1844+
1845+ # Call the /search endpoint without parameters.
1846+ # The result is always as expected in all use cases: two features are returned.
1847+ resp = await app_client .get ("/search" )
1848+ assert resp .status_code == 200
1849+ contents = resp .json ()
1850+ assert contents ["numberReturned" ] == 2
1851+ all_features = contents ["features" ] # save returned features
1852+
1853+ # Call the /search endpoint again, but this time return only one feature per page
1854+ resp = await app_client .get ("/search" , params = {"limit" : 1 })
1855+ contents = None
1856+
1857+ # Loop on all pages/features. For each page, we will:
1858+ # - check the returned feature contents
1859+ # - check the previous link contents, if any
1860+ # - get the next feature
1861+ for page , expected_feature in enumerate (all_features ):
1862+ page += 1
1863+ is_first_page = (page == 1 )
1864+ is_last_page = (page == len (all_features ))
1865+
1866+ # Get returned feature contents, save old feature contents
1867+ previous_contents = contents
1868+ assert resp .status_code == 200
1869+ contents = resp .json ()
1870+
1871+ # Get the previous and next links
1872+ links = contents ["links" ]
1873+ previous_urls = [link ["href" ] for link in links if link ["rel" ] == "previous" ]
1874+ next_urls = [link ["href" ] for link in links if link ["rel" ] == "next" ]
1875+
1876+ # Check that they are present, except for:
1877+ # - the first page has no previous link
1878+ # - the last page has no next link
1879+ if is_first_page :
1880+ assert len (previous_urls ) == 0
1881+ previous_url = None
1882+ else :
1883+ assert len (previous_urls ) == 1
1884+ # NOTE: in the "ko" use case, the "previous" url seems invalid: http://test/search?limit=1&token=prev%3A%3A
1885+ # It should be like: http://test/search?limit=1&token=prev%3Atest-collection%3Atest-item
1886+ previous_url = previous_urls [0 ]
1887+
1888+ if is_last_page :
1889+ assert len (next_urls ) == 0
1890+ next_url = None
1891+ else :
1892+ assert len (next_urls ) == 1
1893+ next_url = next_urls [0 ]
1894+
1895+ print (f"""
1896+ page: { page }
1897+ previous url: { previous_url }
1898+ next url: { next_url }
1899+ numberReturned: { contents ['numberReturned' ]} """ )
1900+
1901+ # A single feature should always be returned. Check its contents.
1902+ assert contents ["numberReturned" ] == 1
1903+ feature = contents ["features" ][0 ]
1904+ print (f"returned feature: { feature ['collection' ]} :{ feature ['id' ]} " )
1905+ assert expected_feature == feature
1906+
1907+ # Check the previous link contents
1908+ if not is_first_page :
1909+ assert previous_contents == (await app_client .get (previous_url )).json ()
1910+
1911+ # Get the next feature
1912+ if not is_last_page :
1913+ resp = await app_client .get (next_url )
0 commit comments