1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import json
1516import logging
1617
17- from rocrate_validator import models
18+ import pytest
19+
20+ from rocrate_validator import models , services
1821from tests .conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER
1922from tests .ro_crates import InvalidDataEntity , ValidROC
2023from tests .shared import do_entity_test
@@ -34,7 +37,7 @@ def test_missing_data_entity_reference():
3437 models .Severity .REQUIRED ,
3538 False ,
3639 ["Data Entity: REQUIRED properties" ],
37- ["sort-and-change-case.ga" , "foo/xxx" ]
40+ ["sort-and-change-case.ga" , "foo/xxx" ],
3841 )
3942
4043
@@ -44,7 +47,7 @@ def test_data_entity_must_be_directly_linked():
4447 paths .direct_hasPart_data_entity_reference ,
4548 models .Severity .REQUIRED ,
4649 True ,
47- skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ]
50+ skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ],
4851 )
4952
5053
@@ -54,7 +57,7 @@ def test_data_entity_not_linked():
5457 paths .dataset_not_linked_to_root ,
5558 models .Severity .REQUIRED ,
5659 False ,
57- skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ]
60+ skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ],
5861 )
5962
6063
@@ -64,7 +67,7 @@ def test_data_entity_must_be_indirectly_linked():
6467 paths .indirect_hasPart_data_entity_reference ,
6568 models .Severity .REQUIRED ,
6669 True ,
67- skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ]
70+ skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ],
6871 )
6972
7073
@@ -75,7 +78,7 @@ def test_directory_data_entity_wo_trailing_slash():
7578 models .Severity .RECOMMENDED ,
7679 False ,
7780 ["Directory Data Entity: RECOMMENDED value restriction" ],
78- ["Every Data Entity Directory URI SHOULD end with `/`" ]
81+ ["Every Data Entity Directory URI SHOULD end with `/`" ],
7982 )
8083
8184
@@ -86,7 +89,7 @@ def test_missing_data_entity_encoding_format():
8689 models .Severity .RECOMMENDED ,
8790 False ,
8891 ["File Data Entity: RECOMMENDED properties" ],
89- ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ]
92+ ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ],
9093 )
9194
9295
@@ -97,7 +100,7 @@ def test_invalid_data_entity_encoding_format_pronom():
97100 models .Severity .RECOMMENDED ,
98101 False ,
99102 ["File Data Entity: RECOMMENDED properties" ],
100- ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ]
103+ ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ],
101104 )
102105
103106
@@ -108,7 +111,7 @@ def test_invalid_data_entity_encoding_format_ctx_website_type():
108111 models .Severity .RECOMMENDED ,
109112 False ,
110113 ["File Data Entity: RECOMMENDED properties" ],
111- ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ]
114+ ["Missing or invalid `encodingFormat` linked to the `File Data Entity`" ],
112115 )
113116
114117
@@ -119,7 +122,7 @@ def test_invalid_data_entity_encoding_format_ctx_website_name():
119122 models .Severity .RECOMMENDED ,
120123 False ,
121124 ["WebSite RECOMMENDED Properties" ],
122- ["A WebSite MUST have a `name` property" ]
125+ ["A WebSite MUST have a `name` property" ],
123126 )
124127
125128
@@ -129,7 +132,7 @@ def test_valid_data_entity_encoding_format_pronom():
129132 paths .valid_encoding_format_pronom ,
130133 models .Severity .RECOMMENDED ,
131134 True ,
132- skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ]
135+ skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ],
133136 )
134137
135138
@@ -139,7 +142,7 @@ def test_valid_data_entity_encoding_format_ctx_website():
139142 paths .valid_encoding_format_ctx_entity ,
140143 models .Severity .RECOMMENDED ,
141144 True ,
142- skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ]
145+ skip_checks = [SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER ],
143146 )
144147
145148
@@ -150,7 +153,7 @@ def test_missing_file_data_entity_with_quoted_name():
150153 models .Severity .REQUIRED ,
151154 False ,
152155 ["Data Entity: REQUIRED resource availability" ],
153- ["The RO-Crate does not include the Data Entity 'pics/2017-06-11%2012.56.14.jpg' as part of its payload" ]
156+ ["The RO-Crate does not include the Data Entity 'pics/2017-06-11%2012.56.14.jpg' as part of its payload" ],
154157 )
155158
156159
@@ -161,7 +164,7 @@ def test_missing_file_data_entity_with_unquoted_name():
161164 models .Severity .REQUIRED ,
162165 False ,
163166 ["Data Entity: REQUIRED resource availability" ],
164- ["The RO-Crate does not include the Data Entity 'pics/2017-06-11 12.56.14.jpg' as part of its payload" ]
167+ ["The RO-Crate does not include the Data Entity 'pics/2017-06-11 12.56.14.jpg' as part of its payload" ],
165168 )
166169
167170
@@ -172,7 +175,7 @@ def test_missing_dataset_entity_with_quoted_name():
172175 models .Severity .REQUIRED ,
173176 False ,
174177 ["Data Entity: REQUIRED resource availability" ],
175- ["The RO-Crate does not include the Data Entity 'data%20set/' as part of its payload" ]
178+ ["The RO-Crate does not include the Data Entity 'data%20set/' as part of its payload" ],
176179 )
177180
178181
@@ -183,7 +186,7 @@ def test_missing_dataset_entity_with_unquoted_name():
183186 models .Severity .REQUIRED ,
184187 False ,
185188 ["Data Entity: REQUIRED resource availability" ],
186- ["The RO-Crate does not include the Data Entity 'data set/' as part of its payload" ]
189+ ["The RO-Crate does not include the Data Entity 'data set/' as part of its payload" ],
187190 )
188191
189192
@@ -194,15 +197,78 @@ def test_missing_absolute_path_data_entity():
194197 models .Severity .RECOMMENDED ,
195198 False ,
196199 ["Data Entity: RECOMMENDED resource availability" ],
197- ["Data Entity file:///tmp/test.txt is not available" ]
200+ ["Data Entity file:///tmp/test.txt is not available" ],
198201 )
199202
200203
201204def test_valid_rocrate_with_data_entities ():
202205 """"""
203- do_entity_test (
204- ValidROC ().rocrate_with_data_entities ,
205- models .Severity .REQUIRED ,
206- True ,
207- profile_identifier = "ro-crate"
206+ do_entity_test (ValidROC ().rocrate_with_data_entities , models .Severity .REQUIRED , True , profile_identifier = "ro-crate" )
207+
208+
209+ @pytest .mark .parametrize (
210+ "remote_entity_id" ,
211+ [
212+ "scp://transfer.example.org//data/A.0.0" ,
213+ "sftp://user@host/path/to/file" ,
214+ "s3://bucket/key" ,
215+ ],
216+ )
217+ def test_remote_data_entity_does_not_fail_required_check (tmp_path , remote_entity_id ):
218+ """Regression test for issue #176.
219+
220+ A Data Entity whose `@id` is an absolute URI with a non-file scheme (e.g.
221+ ``scp://``, ``sftp://``, ``s3://``) MUST NOT trigger the
222+ "Data Entity: REQUIRED resource availability" violation: per the RO-Crate
223+ spec, any absolute-URI Data Entity is web-based and is not required to be
224+ part of the local payload.
225+ """
226+ crate_dir = tmp_path / "crate-with-remote-entity"
227+ crate_dir .mkdir ()
228+ metadata = {
229+ "@context" : "https://w3id.org/ro/crate/1.1/context" ,
230+ "@graph" : [
231+ {
232+ "@id" : "ro-crate-metadata.json" ,
233+ "@type" : "CreativeWork" ,
234+ "conformsTo" : {"@id" : "https://w3id.org/ro/crate/1.1" },
235+ "about" : {"@id" : "./" },
236+ },
237+ {
238+ "@id" : "./" ,
239+ "@type" : "Dataset" ,
240+ "name" : "Crate with remote entity" ,
241+ "description" : "Regression fixture for issue #176" ,
242+ "datePublished" : "2026-05-15T07:30:50+00:00" ,
243+ "license" : {"@id" : "https://spdx.org/licenses/CC0-1.0" },
244+ "hasPart" : [{"@id" : remote_entity_id }],
245+ },
246+ {
247+ "@id" : remote_entity_id ,
248+ "@type" : "File" ,
249+ "name" : "Remote file" ,
250+ "contentSize" : 16 ,
251+ "dateModified" : "2026-05-15T07:30:50+00:00" ,
252+ "sdDatePublished" : "2026-05-15T07:31:03+00:00" ,
253+ },
254+ {"@id" : "https://spdx.org/licenses/CC0-1.0" , "@type" : "CreativeWork" , "name" : "CC0" },
255+ ],
256+ }
257+ (crate_dir / "ro-crate-metadata.json" ).write_text (json .dumps (metadata ))
258+
259+ result = services .validate (
260+ models .ValidationSettings (
261+ rocrate_uri = crate_dir ,
262+ requirement_severity = models .Severity .REQUIRED ,
263+ profile_identifier = "ro-crate" ,
264+ )
265+ )
266+ assert result .passed (), (
267+ f"RO-Crate with remote entity '{ remote_entity_id } ' should pass REQUIRED "
268+ f"validation; got issues: { [i .message for i in result .get_issues ()]} "
208269 )
270+ # And the specific must/4 violation must NOT be among the issues.
271+ for issue in result .get_issues ():
272+ assert "as part of its payload" not in (issue .message or "" ), (
273+ f"Unexpected payload violation raised for remote entity: { issue .message } "
274+ )
0 commit comments