@@ -149,6 +149,151 @@ def test_system_full_chain_geojson_dict_to_resource_to_wrapper(node):
149149 assert sys .name == "GeoSys2"
150150
151151
152+ # ---------------------------------------------------------------------------
153+ # SML type preservation and non-mutation
154+ # ---------------------------------------------------------------------------
155+
156+ def test_to_smljson_preserves_non_default_feature_type ():
157+ """A source whose SML type is ``PhysicalComponent`` (which OSH
158+ surfaces as ``featureType: Sensor``) must round-trip through
159+ ``to_smljson_dict`` without being collapsed back to
160+ ``PhysicalSystem``. Regression guard for cross-node sync."""
161+ src = SystemResource (uid = "urn:test:s1" , label = "S1" ,
162+ feature_type = "PhysicalComponent" )
163+ dumped = src .to_smljson_dict ()
164+ assert dumped ["type" ] == "PhysicalComponent"
165+
166+
167+ def test_to_smljson_defaults_to_physical_system_when_unset ():
168+ """When ``feature_type`` is unset, the SML body still gets a
169+ sensible default so callers building a bare SystemResource
170+ continue to produce a valid SML body."""
171+ src = SystemResource (uid = "urn:test:s1" , label = "S1" )
172+ dumped = src .to_smljson_dict ()
173+ assert dumped ["type" ] == "PhysicalSystem"
174+
175+
176+ def test_to_smljson_does_not_mutate_feature_type ():
177+ """Pre-fix, ``to_smljson_dict`` set ``self.feature_type`` as a
178+ side effect, which clobbered the source's SML kind. After the
179+ fix, the model is untouched."""
180+ src = SystemResource (uid = "urn:test:s1" , label = "S1" ,
181+ feature_type = "PhysicalComponent" )
182+ src .to_smljson_dict ()
183+ assert src .feature_type == "PhysicalComponent"
184+
185+
186+ def test_to_geojson_always_emits_feature_without_mutating ():
187+ """GeoJSON form requires ``type: Feature`` per spec, regardless
188+ of ``feature_type`` on the model. The model itself stays
189+ unmutated."""
190+ src = SystemResource (uid = "urn:test:s1" , label = "S1" ,
191+ feature_type = "PhysicalComponent" )
192+ dumped = src .to_geojson_dict ()
193+ assert dumped ["type" ] == "Feature"
194+ assert src .feature_type == "PhysicalComponent"
195+
196+
197+ # ---------------------------------------------------------------------------
198+ # System.to_system_resource preserves _underlying_resource
199+ # ---------------------------------------------------------------------------
200+
201+ def test_to_system_resource_preserves_full_underlying (node ):
202+ """When the wrapper carries a full ``_underlying_resource`` (e.g.,
203+ populated by discovery / ``from_csapi_dict``), the resource
204+ rendered for POST keeps every field — not just uid/label/type."""
205+ raw = {
206+ "type" : "PhysicalComponent" ,
207+ "id" : "src-server-id-abc" ,
208+ "uniqueId" : "urn:test:source:1" ,
209+ "label" : "Source Sensor" ,
210+ "description" : "Original description" ,
211+ "definition" : "http://www.opengis.net/def/system" ,
212+ "keywords" : ["thermal" , "imaging" ],
213+ }
214+ res = SystemResource .from_smljson_dict (raw )
215+ sys = System .from_resource (res , node )
216+
217+ rendered = sys .to_system_resource ()
218+
219+ # Type preserved (was hardcoded to PhysicalSystem pre-fix).
220+ assert rendered .feature_type == "PhysicalComponent"
221+ # Other fields preserved (were silently dropped pre-fix).
222+ assert rendered .description == "Original description"
223+ assert rendered .definition == "http://www.opengis.net/def/system"
224+ assert rendered .keywords == ["thermal" , "imaging" ]
225+
226+
227+ def test_to_system_resource_thin_shell_for_freshly_constructed (node ):
228+ """A System constructed from scratch (no parsed resource) still
229+ produces a sensible thin shell with default ``PhysicalSystem``
230+ type — backward-compat with code that doesn't go through
231+ discovery."""
232+ sys = System (name = "Fresh" , label = "Fresh" , urn = "urn:test:fresh:1" ,
233+ parent_node = node )
234+ rendered = sys .to_system_resource ()
235+ assert rendered .feature_type == "PhysicalSystem"
236+ assert rendered .uid == "urn:test:fresh:1"
237+
238+
239+ # ---------------------------------------------------------------------------
240+ # insert_self strips server-assigned fields from the POST body
241+ # ---------------------------------------------------------------------------
242+
243+ class _MockResponse :
244+ status_code = 201
245+ ok = True
246+ text = ""
247+ headers = {"Location" : "http://localhost:8282/sensorhub/api/systems/dest-id-xyz" }
248+
249+
250+ def _capture_post (into : dict ):
251+ def _f (url , params = None , headers = None , auth = None , data = None , json = None , ** kwargs ):
252+ into ["url" ] = str (url )
253+ into ["data" ] = data
254+ into ["json" ] = json
255+ return _MockResponse ()
256+ return _f
257+
258+
259+ def test_insert_self_strips_id_and_links_from_body (node , monkeypatch ):
260+ """When re-POSTing a discovered system to a destination node, the
261+ source's server-assigned ``id`` and ``links`` must not leak into
262+ the body — the destination assigns its own. Regression guard for
263+ cross-node sync."""
264+ raw = {
265+ "type" : "PhysicalComponent" ,
266+ "id" : "source-side-id" ,
267+ "uniqueId" : "urn:test:source:1" ,
268+ "label" : "Source Sensor" ,
269+ "links" : [{"href" : "http://source.example/extra" , "rel" : "alternate" }],
270+ }
271+ res = SystemResource .from_smljson_dict (raw )
272+ sys = System .from_resource (res , node )
273+
274+ captured : dict = {}
275+ monkeypatch .setattr (
276+ "oshconnect.csapi4py.request_wrappers.requests.post" ,
277+ _capture_post (captured ),
278+ )
279+
280+ sys .insert_self ()
281+
282+ body = json .loads (captured ["data" ])
283+ # Source-assigned identifiers must NOT be present in the POST body.
284+ assert "id" not in body , (
285+ "POST body must not carry source's server-assigned id"
286+ )
287+ assert "links" not in body , (
288+ "POST body must not carry source's server-assigned links"
289+ )
290+ # But the SML kind from the source IS preserved.
291+ assert body ["type" ] == "PhysicalComponent"
292+ assert body ["uniqueId" ] == "urn:test:source:1"
293+ # Wrapper picked up the destination's id from the Location header.
294+ assert sys ._resource_id == "dest-id-xyz"
295+
296+
152297# ===========================================================================
153298# Datastream: resource representation, schema document, observations
154299# ===========================================================================
0 commit comments