Repository: CS-SI/eodag
Pull request #1174 (2024-05-30 16:25:56)

fix: improve stac collection links generation
### Description

This PR provides an additional fix for the generation of a stac collection.

The links created by the stac config were erased when updating the dictionary contents, resulting in a 500 InternalServerError when trying to retrieve the list of collections if one of the collection was generated using an external stac collection.


github-actions[bot]: ## Test Results
  2 files   -     2    2 suites   - 2   1m 51s :stopwatch: - 3m 37s
539 tests ±    0  535 :white_check_mark:  -     1   3 :zzz: ± 0  1 :x: +1 
792 runs   - 1 364  745 :white_check_mark:  - 1 313  46 :zzz:  - 52  1 :x: +1 

For more details on these failures, see [this check](https://github.com/CS-SI/eodag/runs/25614181739).

Results for commit 2b52987f. ± Comparison against base commit 04546522.

[test-results]:data:application/gzip;base64,H4sIAGapWGYC/03MTQ7CIBCG4as0rF0ABaZ4GQMUEmJ/DIVV492dVsEuZvE9k7w7CXHyG7l3/NaRrcTcxliSyXFdcDLGEPCVj6fsdV2PrTh3kvzTM76Q+gbBxOmINPAprQmFoqSyHE3Q/DdqEoRs8i0KVeFSPPc16NZ5jhkH4VZyPUCAoCxoqq1VVNpgPB9GPOqEGDWAJe8Pidt/9wYBAAA=

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/fix-external-product-types-links/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         302      51  83.11%   60, 644-690, 792-843, 847
config.py                                      349      27  92.26%   81-83, 92, 100, 104-106, 183, 195, 452-454, 518-521, 568-569, 578-579, 658, 727-732, 734
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    745      77  89.66%   88-97, 371, 596, 640-643, 681, 718, 790, 794-799, 825, 921, 998, 1111, 1201-1213, 1253, 1255, 1259, 1282, 1286-1297, 1310-1316, 1406-1409, 1438-1458, 1515, 1532-1535, 1547-1550, 1918, 1951-1957, 2222, 2226-2229, 2243-2245, 2280
api/search_result.py                            44       6  86.36%   33-35, 70, 79, 86, 100
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       5  88.64%   27-29, 79, 129
api/product/_product.py                        203      32  84.24%   52-59, 63-65, 169-176, 260-261, 343, 372, 433, 447-450, 463, 487-490, 533-539
api/product/metadata_mapping.py                677      83  87.74%   67-69, 130-132, 233, 265-266, 312-313, 323-335, 337, 348, 354-366, 409-410, 447, 468-471, 494, 502-503, 579-580, 604-605, 611-614, 629-630, 779, 825, 999-1004, 1135, 1149-1169, 1189, 1194, 1304, 1326, 1340, 1353-1372, 1411, 1463, 1501-1505, 1524
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       3  86.96%   25, 48, 55
plugins/manager.py                             133      15  88.72%   49-51, 105-110, 156, 197, 219, 223, 248, 287-288
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           98      15  84.69%   47-55, 156-158, 205-206, 232-234
plugins/apis/usgs.py                           172      36  79.07%   59-64, 128, 201, 235, 270-272, 277, 303-304, 309, 339-346, 357-362, 384-390, 392-398, 421
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       3  84.21%   26, 34, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                17       1  94.12%   27
plugins/authentication/keycloak.py              88      17  80.68%   32-34, 159-160, 190-212, 238-243
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       117      11  90.60%   40-42, 153, 159, 172-173, 184-189
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 90      17  81.11%   35-37, 79, 107, 109, 131-143, 198-201
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      10  80.00%   32-34, 51-52, 71, 80-83, 85, 92-95
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      17  75.00%   28-30, 33, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      57  78.08%   58-64, 145, 180, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       529     132  75.05%   85-91, 124, 203-213, 215-216, 251-254, 318-321, 323-324, 331-336, 354-369, 386-388, 400, 448, 455-461, 479, 493, 507, 515-517, 533-538, 549, 567, 609-613, 635, 675, 720, 734-740, 776-837, 855, 888-897, 923-924, 951-956, 962, 978, 995-996, 1023-1024, 1031, 1093-1099, 1154-1155, 1161, 1171, 1237, 1241, 1255-1271
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                       0       0  100.00%
plugins/search/base.py                         132      13  90.15%   49-54, 108, 112, 125, 275, 295, 354-355, 375, 384
plugins/search/build_search_result.py          179      24  86.59%   62, 97, 142-143, 149, 160, 296-299, 333, 390-407, 469, 472, 482, 499, 527, 529
plugins/search/creodias_s3.py                   54       3  94.44%   55, 73, 107
plugins/search/csw.py                          107      83  22.43%   43-45, 57-58, 62-63, 74-122, 128-141, 149-181, 199-240
plugins/search/data_request_search.py          197      65  67.01%   52, 89-92, 108, 120, 124-125, 139, 144, 149, 156, 169-172, 226-227, 231, 241-247, 252, 278-281, 289-300, 317, 319, 326-327, 329-330, 348-352, 385, 392, 403, 416, 422-434, 439
plugins/search/qssearch.py                     606      67  88.94%   101, 373-379, 387-388, 495-501, 549, 565, 575, 602, 604, 654-657, 730-731, 772, 791, 806, 859, 880, 883-884, 893, 904, 913, 936, 998-1003, 1007-1008, 1036, 1107, 1154, 1230-1234, 1300-1301, 1322, 1347-1359, 1366, 1397-1398, 1409-1415, 1458, 1473, 1493, 1594-1604
plugins/search/static_stac_search.py            47       3  93.62%   39-40, 82
rest/__init__.py                                 4       2  50.00%   21-22
rest/core.py                                   207      17  91.79%   66-72, 261, 345, 605, 607, 610-612, 684, 691-696
rest/server.py                                 299      53  82.27%   80-81, 107, 130-131, 244-246, 302-303, 315-331, 423-428, 460, 627-634, 667, 711-712, 803-805, 822-827, 858, 860, 864-865, 869-870
rest/stac.py                                   459      91  80.17%   62-64, 278, 300, 372, 409-411, 434, 469-470, 556-595, 632-637, 701-703, 738, 758-759, 902, 967-969, 1188, 1198-1210, 1223-1245, 1259-1304, 1463-1464
rest/types/__init__.py                           0       0  100.00%
rest/types/eodag_search.py                     184       9  95.11%   52-55, 235-239, 292, 295, 363
rest/types/stac_queryables.py                   32       2  93.75%   28, 116
rest/types/stac_search.py                      121      10  91.74%   48-51, 167, 182-184, 192, 196
rest/utils/__init__.py                          95      13  86.32%   51, 108-109, 128-130, 182, 192-206
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           31       3  90.32%   78, 90, 92
types/__init__.py                              110      11  90.00%   53, 70, 129-132, 199, 214-218, 245, 258
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      42  91.62%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1102, 1186, 1204, 1380
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
utils/exceptions.py                             37       2  94.59%   23, 93
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/requests.py                               55      11  80.00%   69, 96, 98, 100, 102, 104, 123, 128-130, 138
utils/rest.py                                   27       1  96.30%   49
utils/stac_reader.py                            91      28  69.23%   55-56, 63-86, 93-95, 99, 141, 155-158
TOTAL                                         9070    1563  82.77%
```

</details>


### Diff against develop

```
Filename        Stmts    Miss  Cover
------------  -------  ------  -------
rest/stac.py       +5      +3  -0.45%
TOTAL              +5      +3  -0.02%
```


Results for commit: f0949ef460972ac1f9aeccfb247572ede38aa127


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-ubuntu-latest/coverage.xml -->
github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports_win/data/fix-external-product-types-links/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         302      51  83.11%   60, 644-690, 792-843, 847
config.py                                      349      28  91.98%   81-83, 92, 100, 104-106, 183, 195, 452-454, 518-521, 568-569, 578-579, 658, 692, 727-732, 734
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    745      77  89.66%   88-97, 371, 596, 640-643, 681, 718, 790, 794-799, 825, 921, 998, 1111, 1201-1213, 1253, 1255, 1259, 1282, 1286-1297, 1310-1316, 1406-1409, 1438-1458, 1515, 1532-1535, 1547-1550, 1918, 1951-1957, 2222, 2226-2229, 2243-2245, 2280
api/search_result.py                            44       6  86.36%   33-35, 70, 79, 86, 100
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       5  88.64%   27-29, 79, 129
api/product/_product.py                        203      32  84.24%   52-59, 63-65, 169-176, 260-261, 343, 372, 433, 447-450, 463, 487-490, 533-539
api/product/metadata_mapping.py                677      83  87.74%   67-69, 130-132, 233, 265-266, 312-313, 323-335, 337, 348, 354-366, 409-410, 447, 468-471, 494, 502-503, 579-580, 604-605, 611-614, 629-630, 779, 825, 999-1004, 1135, 1149-1169, 1189, 1194, 1304, 1326, 1340, 1353-1372, 1411, 1463, 1501-1505, 1524
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       4  82.61%   25, 48, 55, 68
plugins/manager.py                             133      15  88.72%   49-51, 105-110, 156, 197, 219, 223, 248, 287-288
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           98      15  84.69%   47-55, 156-158, 205-206, 232-234
plugins/apis/usgs.py                           172      36  79.07%   59-64, 128, 201, 235, 270-272, 277, 303-304, 309, 339-346, 357-362, 384-390, 392-398, 421
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       3  84.21%   26, 34, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                17       1  94.12%   27
plugins/authentication/keycloak.py              88      17  80.68%   32-34, 159-160, 190-212, 238-243
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       117      11  90.60%   40-42, 153, 159, 172-173, 184-189
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 90      17  81.11%   35-37, 79, 107, 109, 131-143, 198-201
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      35  30.00%   32-34, 48-53, 69-114
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      17  75.00%   28-30, 33, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      59  77.31%   58-64, 145, 180, 250-252, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       529     132  75.05%   85-91, 124, 203-213, 215-216, 251-254, 318-321, 323-324, 331-336, 354-369, 386-388, 400, 448, 455-461, 479, 493, 507, 515-517, 533-538, 549, 567, 609-613, 635, 675, 720, 734-740, 776-837, 855, 888-897, 923-924, 951-956, 962, 978, 995-996, 1023-1024, 1031, 1093-1099, 1154-1155, 1161, 1171, 1237, 1241, 1255-1271
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                       0       0  100.00%
plugins/search/base.py                         132      18  86.36%   49-54, 108, 112, 125, 275, 295, 354-355, 375, 378-386, 388
plugins/search/build_search_result.py          179      31  82.68%   62, 97, 142-143, 149, 160, 296-299, 333, 390-407, 469, 472, 482, 499, 519-534
plugins/search/creodias_s3.py                   54       3  94.44%   55, 73, 107
plugins/search/csw.py                          107      83  22.43%   43-45, 57-58, 62-63, 74-122, 128-141, 149-181, 199-240
plugins/search/data_request_search.py          197      65  67.01%   52, 89-92, 108, 120, 124-125, 139, 144, 149, 156, 169-172, 226-227, 231, 241-247, 252, 278-281, 289-300, 317, 319, 326-327, 329-330, 348-352, 385, 392, 403, 416, 422-434, 439
plugins/search/qssearch.py                     606      95  84.32%   101, 373-379, 387-388, 495-501, 549, 552, 565, 575, 594-609, 654-657, 730-731, 772, 791, 806, 859, 880, 883-884, 893, 904, 913, 936, 998-1003, 1007-1008, 1036, 1107, 1154, 1230-1234, 1300-1301, 1322, 1347-1359, 1366, 1397-1398, 1409-1415, 1458, 1473, 1493, 1555-1624
plugins/search/static_stac_search.py            47       3  93.62%   39-40, 82
rest/__init__.py                                 4       2  50.00%   21-22
rest/core.py                                   207      96  53.62%   66-72, 153, 155, 157, 163-164, 185-193, 199-205, 252-306, 319-355, 505-536, 554, 604-643, 684, 691-696
rest/server.py                                 299     299  0.00%    18-883
rest/stac.py                                   459     150  67.32%   62-64, 231, 278, 300, 372, 409-411, 434, 469-470, 556-595, 632-637, 701-703, 738, 746-747, 751-759, 844, 902, 967-969, 986-988, 996-998, 1011-1013, 1027-1044, 1054-1075, 1085-1107, 1115-1132, 1155-1178, 1188, 1198-1210, 1223-1245, 1259-1304, 1457-1483
rest/types/__init__.py                           0       0  100.00%
rest/types/eodag_search.py                     184      18  90.22%   52-55, 235-239, 272-274, 292, 295, 301, 305, 363, 375-378
rest/types/stac_queryables.py                   32       6  81.25%   28, 55-60, 116
rest/types/stac_search.py                      121      12  90.08%   48-51, 167, 182-184, 192, 196, 240, 243
rest/utils/__init__.py                          95      31  67.37%   51, 79-85, 105, 108-109, 128-130, 143, 150, 175-183, 190-211
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           31       5  83.87%   73-74, 78, 90, 92
types/__init__.py                              110      36  67.27%   53, 66-70, 81-93, 120-122, 129-132, 172, 199, 209-221, 226, 245, 250, 258, 268
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      43  91.42%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1102, 1185-1186, 1204, 1380
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
utils/exceptions.py                             37       2  94.59%   23, 93
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/requests.py                               55      11  80.00%   69, 96, 98, 100, 102, 104, 123, 128-130, 138
utils/rest.py                                   27       1  96.30%   49
utils/stac_reader.py                            91      28  69.23%   55-56, 63-86, 93-95, 99, 141, 155-158
TOTAL                                         9070    2077  77.10%
```

</details>


### Diff against develop

```
Filename        Stmts    Miss  Cover
------------  -------  ------  -------
rest/stac.py       +5      +3  -0.30%
TOTAL              +5      +3  -0.02%
```


Results for commit: f0949ef460972ac1f9aeccfb247572ede38aa127


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-windows-latest/coverage.xml -->

Patch:
diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py
index b754f75e..37d17df0 100644
--- a/eodag/rest/stac.py
+++ b/eodag/rest/stac.py
@@ -35,7 +35,6 @@ from shapely.ops import unary_union
 
 from eodag.api.product.metadata_mapping import (
     DEFAULT_METADATA_MAPPING,
-    NOT_AVAILABLE,
     format_metadata,
     get_metadata_path,
 )
@@ -655,6 +654,80 @@ class StacCollection(StacCommon):
             root=root,
         )
 
+    def __list_product_type_providers(self, product_type: Dict[str, Any]) -> List[str]:
+        """Retrieve a list of providers for a given product type.
+
+        :param product_type: Dictionary containing information about the product type.
+        :type product_type: dict
+        :return: A list of provider names.
+        :rtype: list
+        """
+        if self.provider:
+            return [self.provider]
+
+        return [
+            plugin.provider
+            for plugin in self.eodag_api._plugins_manager.get_search_plugins(
+                product_type=product_type.get("_id", product_type["ID"])
+            )
+        ]
+
+    def __generate_stac_collection(
+        self, collection_model: Any, product_type: Dict[str, Any]
+    ) -> Dict[str, Any]:
+        """Generate a STAC collection dictionary for a given product type.
+
+        :param collection_model: The base model for the STAC collection.
+        :type collection_model: Any
+        :param product_type: Dictionary containing information about the product type.
+        :type product_type: dict
+        :return: A dictionary representing the STAC collection for the given product type.
+        :rtype: dict
+        """
+        providers = self.__list_product_type_providers(product_type)
+
+        # parse jsonpath
+        product_type_collection = jsonpath_parse_dict_items(
+            collection_model,
+            {
+                "product_type": product_type,
+                "providers": [self.get_provider_dict(p) for p in providers],
+            },
+        )
+        # override EODAG's collection with the external collection
+        ext_stac_collection = self.ext_stac_collections.get(product_type["ID"], {})
+
+        # update links
+        ext_stac_collection.setdefault("links", {})
+        for link in product_type_collection["links"]:
+            ext_stac_collection["links"] = [
+                x for x in ext_stac_collection["links"] if x["rel"] != link["rel"]
+            ]
+            ext_stac_collection["links"].append(link)
+
+        # merge "keywords" lists
+        if "keywords" in ext_stac_collection:
+            ext_stac_collection["keywords"] = list(
+                set(
+                    ext_stac_collection["keywords"]
+                    + product_type_collection["keywords"]
+                )
+            )
+
+        product_type_collection.update(ext_stac_collection)
+
+        # parse f-strings
+        format_args = deepcopy(self.stac_config)
+        format_args["collection"] = {
+            **product_type_collection,
+            **{"url": f"{self.url}/{product_type['ID']}", "root": self.root},
+        }
+        product_type_collection = format_dict_items(
+            product_type_collection, **format_args
+        )
+
+        return product_type_collection
+
     def __get_product_types(
         self, filters: Optional[Dict[str, Any]] = None
     ) -> List[Dict[str, Any]]:
@@ -716,53 +789,10 @@ class StacCollection(StacCommon):
 
         collection_list: List[Dict[str, Any]] = []
         for product_type in product_types:
-            # get available providers for each product_type
-            providers = (
-                [self.provider]
-                if self.provider
-                else [
-                    plugin.provider
-                    for plugin in self.eodag_api._plugins_manager.get_search_plugins(
-                        product_type=(
-                            product_type.get("_id", None) or product_type["ID"]
-                        )
-                    )
-                ]
-            )
-
-            # parse jsonpath
-            product_type_collection = jsonpath_parse_dict_items(
-                collection_model,
-                {
-                    "product_type": product_type,
-                    "providers": [self.get_provider_dict(p) for p in providers],
-                },
-            )
-            # override EODAG's collection with the external collection
-            ext_stac_collection = self.ext_stac_collections.get(product_type["ID"], {})
-
-            # merge "keywords" list
-            merged_keywords = product_type_collection.get("keywords", [])
-            if "keywords" in ext_stac_collection:
-                new_keywords = ext_stac_collection["keywords"]
-                for v in new_keywords:
-                    if v != NOT_AVAILABLE and v not in merged_keywords:
-                        merged_keywords.append(v)
-            product_type_collection.update(ext_stac_collection)
-            if merged_keywords:
-                product_type_collection["keywords"] = merged_keywords
-
-            # parse f-strings
-            format_args = deepcopy(self.stac_config)
-            format_args["collection"] = {
-                **product_type_collection,
-                **{"url": f"{self.url}/{product_type['ID']}", "root": self.root},
-            }
-            product_type_collection = format_dict_items(
-                product_type_collection, **format_args
+            stac_collection = self.__generate_stac_collection(
+                collection_model, product_type
             )
-
-            collection_list.append(product_type_collection)
+            collection_list.append(stac_collection)
 
         return collection_list
 


Test patch:
diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py
index 8945fcfd..57ad4f14 100644
--- a/tests/units/test_stac_utils.py
+++ b/tests/units/test_stac_utils.py
@@ -255,10 +255,15 @@ class TestStacUtils(unittest.TestCase):
 
     def test_fetch_external_stac_collections(self):
         """Load external STAC collections"""
+
         external_json = """{
             "new_field":"New Value",
             "title":"A different title for Sentinel 2 MSI Level 1C",
-            "keywords":["New Keyword"]
+            "keywords":["New Keyword"],
+            "links":[
+                {"rel":"self","href":"http://another.self"},
+                {"rel":"license","href":"http://foo.bar"}
+            ]
         }"""
         product_type_conf = self.rest_core.eodag_api.product_types_config["S2_MSI_L1C"]
         ext_stac_collection_path = "/path/to/external/stac/collections/S2_MSI_L1C.json"
@@ -275,14 +280,24 @@ class TestStacUtils(unittest.TestCase):
                 url="", root="", collection_id="S2_MSI_L1C"
             )
             mock_fetch_json.assert_called_with(ext_stac_collection_path)
+
             # New field
             self.assertIn("new_field", stac_coll)
-            # Merge keywords
+
+            # links
+            self.assertEqual(len(stac_coll["links"]), 4)
+            links_self = [x for x in stac_coll["links"] if x["rel"] == "self"]
+            self.assertEqual(len(links_self), 1)
+            self.assertNotEqual(links_self[0]["href"], "http://another.self")
+            links_license = [x for x in stac_coll["links"] if x["rel"] == "license"]
+            self.assertEqual(links_license[0]["href"], "http://foo.bar")
+
+            # Merged keywords
             self.assertListEqual(
-                ["MSI", "SENTINEL2", "S2A,S2B", "L1", "OPTICAL", "New Keyword"],
-                stac_coll["keywords"],
+                ["L1", "MSI", "New Keyword", "OPTICAL", "S2A,S2B", "SENTINEL2"],
+                sorted(stac_coll["keywords"]),
             )
-            # Override existing fields
+            # Overriden existing fields
             self.assertEqual(
                 "A different title for Sentinel 2 MSI Level 1C", stac_coll["title"]
             )