Skip to content

Commit e4ea8b0

Browse files
committed
Merge branch 'main' into test-data-for-demo
2 parents 6f3dc9e + bae6ff7 commit e4ea8b0

File tree

15 files changed

+528
-259
lines changed

15 files changed

+528
-259
lines changed

DCO.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Developer Certificate of Origin
2+
Version 1.1
3+
4+
By making a contribution to the Nuts Knooppunt project, I certify that:
5+
(a) The contribution was created in whole or in part by me and I
6+
have the right to submit it under the EUPL v1.2 license; or
7+
(b) The contribution is based upon previous work that is covered by
8+
an appropriate open source license and I have the right to submit
9+
that work with modifications under the EUPL v1.2 license; or
10+
(c) The contribution was provided directly to me by some other
11+
person who certified (a), (b) or (c) and I have not modified it.
12+
(d) I understand and agree that this project and the contribution
13+
are public and that a record of the contribution is maintained
14+
indefinitely and may be redistributed under the EUPL v1.2.

LICENSE

Lines changed: 291 additions & 201 deletions
Large diffs are not rendered by default.

component/mcsd/component.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ func (c *Component) updateFromDirectory(ctx context.Context, fhirBaseURLRaw stri
229229

230230
var report DirectoryUpdateReport
231231
for i, entry := range deduplicatedEntries {
232+
if entry.Request == nil {
233+
msg := fmt.Sprintf("Skipping entry with no request: #%d", i)
234+
report.Warnings = append(report.Warnings, msg)
235+
continue
236+
}
232237
log.Ctx(ctx).Trace().Str("fhir_server", fhirBaseURLRaw).Msgf("Processing entry: %s", entry.Request.Url)
233238
resourceType, err := buildUpdateTransaction(ctx, &tx, entry, allowedResourceTypes, allowDiscovery, fhirBaseURLRaw)
234239
if err != nil {

component/mcsd/component_test.go

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -340,60 +340,6 @@ func TestComponent_incrementalUpdates(t *testing.T) {
340340
require.Equal(t, lastUpdate, sinceParams[2], "_since parameter should match the stored lastUpdate timestamp")
341341
}
342342

343-
func TestComponent_noDuplicateResourcesInTransactionBundle(t *testing.T) {
344-
// This test verifies that when _history returns multiple versions of the same resource,
345-
// the transaction bundle sent to the query directory contains no duplicates.
346-
// This addresses the HAPI error: "Transaction bundle contains multiple resources with ID: urn:uuid:..."
347-
emptyResponse, err := os.ReadFile("test/regression_lrza_empty_history_response.json")
348-
require.NoError(t, err)
349-
historyWithDuplicatesBytes, err := os.ReadFile("test/history_with_duplicates.json")
350-
require.NoError(t, err)
351-
352-
mockMux := http.NewServeMux()
353-
// Convert []byte responses to strings for pointer approach
354-
historyWithDuplicatesStr := string(historyWithDuplicatesBytes)
355-
emptyResponseStr3 := string(emptyResponse)
356-
357-
mockHistoryEndpoints(mockMux, map[string]*string{
358-
"/Organization/_history": &historyWithDuplicatesStr,
359-
"/Location/_history": &emptyResponseStr3,
360-
"/Endpoint/_history": &emptyResponseStr3,
361-
"/HealthcareService/_history": &emptyResponseStr3,
362-
})
363-
mockServer := httptest.NewServer(mockMux)
364-
defer mockServer.Close()
365-
366-
capturingClient := &test.StubFHIRClient{}
367-
component, err := New(Config{
368-
QueryDirectory: DirectoryConfig{FHIRBaseURL: "http://example.com/local/fhir"},
369-
})
370-
require.NoError(t, err)
371-
372-
// Register as discovered directory to avoid Organization filtering
373-
err = component.registerAdministrationDirectory(context.Background(), mockServer.URL, []string{"Organization", "Endpoint"}, false)
374-
require.NoError(t, err)
375-
376-
component.fhirClientFn = func(baseURL *url.URL) fhirclient.Client {
377-
if baseURL.String() == mockServer.URL {
378-
return fhirclient.New(baseURL, http.DefaultClient, &fhirclient.Config{UsePostSearch: false})
379-
}
380-
if baseURL.String() == "http://example.com/local/fhir" {
381-
return capturingClient
382-
}
383-
return &test.StubFHIRClient{Error: errors.New("unknown URL")}
384-
}
385-
386-
ctx := context.Background()
387-
report, err := component.update(ctx)
388-
389-
require.NoError(t, err)
390-
require.Empty(t, report[mockServer.URL].Errors, "Should not have errors after deduplication")
391-
392-
// Should have 0 Organizations because the DELETE operation is the most recent
393-
orgs := capturingClient.CreatedResources["Organization"]
394-
require.Len(t, orgs, 0, "Should have 0 Organizations after deduplication (DELETE is most recent operation)")
395-
}
396-
397343
func TestExtractResourceIDFromURL(t *testing.T) {
398344
tests := []struct {
399345
name string
@@ -604,3 +550,82 @@ func TestGetLastUpdated(t *testing.T) {
604550
})
605551
}
606552
}
553+
554+
func TestComponent_updateFromDirectory(t *testing.T) {
555+
ctx := context.Background()
556+
557+
t.Run("#233: no entry.Request in _history results", func(t *testing.T) {
558+
t.Log("See https://github.com/nuts-foundation/nuts-knooppunt/issues/233")
559+
server := startMockServer(t, map[string]string{
560+
"/fhir/Organization/_history": "test/bugs/233-no-bundle-request/organization_response.json",
561+
})
562+
component, err := New(Config{})
563+
require.NoError(t, err)
564+
report, err := component.updateFromDirectory(ctx, server.URL+"/fhir", []string{"Organization"}, false)
565+
require.NoError(t, err)
566+
require.NotNil(t, report)
567+
require.Len(t, report.Warnings, 1)
568+
assert.Equal(t, report.Warnings[0], "Skipping entry with no request: #0")
569+
assert.Empty(t, report.Errors)
570+
assert.Equal(t, 0, report.CountCreated)
571+
assert.Equal(t, 0, report.CountUpdated)
572+
assert.Equal(t, 0, report.CountDeleted)
573+
})
574+
575+
t.Run("no duplicate resources in transaction bundle", func(t *testing.T) {
576+
// This test verifies that when _history returns multiple versions of the same resource,
577+
// the transaction bundle sent to the query directory contains no duplicates.
578+
// This addresses the HAPI error: "Transaction bundle contains multiple resources with ID: urn:uuid:..."
579+
server := startMockServer(t, map[string]string{
580+
"/fhir/Organization/_history": "test/history_with_duplicates.json",
581+
})
582+
defer server.Close()
583+
584+
capturingClient := &test.StubFHIRClient{}
585+
component, err := New(Config{})
586+
require.NoError(t, err)
587+
588+
component.fhirClientFn = func(baseURL *url.URL) fhirclient.Client {
589+
if baseURL.String() == server.URL+"/fhir" {
590+
return fhirclient.New(baseURL, http.DefaultClient, &fhirclient.Config{UsePostSearch: false})
591+
}
592+
if baseURL.String() == "http://example.com/local/fhir" {
593+
return capturingClient
594+
}
595+
return &test.StubFHIRClient{Error: errors.New("unknown URL")}
596+
}
597+
598+
report, err := component.updateFromDirectory(ctx, server.URL+"/fhir", []string{"Organization", "Endpoint"}, false)
599+
600+
require.NoError(t, err)
601+
require.Empty(t, report.Errors, "Should not have errors after deduplication")
602+
603+
// Should have 0 Organizations because the DELETE operation is the most recent
604+
orgs := capturingClient.CreatedResources["Organization"]
605+
require.Len(t, orgs, 0, "Should have 0 Organizations after deduplication (DELETE is most recent operation)")
606+
})
607+
}
608+
609+
func startMockServer(t *testing.T, filesToServe map[string]string) *httptest.Server {
610+
mux := http.NewServeMux()
611+
server := httptest.NewServer(mux)
612+
613+
emptyBundleData, err := os.ReadFile("test/empty_bundle_response.json")
614+
require.NoError(t, err)
615+
emptyResponseStr := string(emptyBundleData)
616+
pathsToServe := map[string]*string{
617+
"/fhir/Endpoint/_history": &emptyResponseStr,
618+
"/fhir/Organization/_history": &emptyResponseStr,
619+
"/fhir/Location/_history": &emptyResponseStr,
620+
"/fhir/HealthcareService/_history": &emptyResponseStr,
621+
}
622+
for path, filename := range filesToServe {
623+
data, err := os.ReadFile(filename)
624+
require.NoError(t, err)
625+
dataStr := string(data)
626+
pathsToServe[path] = &dataStr
627+
}
628+
629+
mockHistoryEndpoints(mux, pathsToServe)
630+
return server
631+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"resourceType": "Bundle",
3+
"meta": {
4+
"lastUpdated": "2025-08-14T10:00:00.000+00:00"
5+
},
6+
"type": "history",
7+
"entry": [
8+
{
9+
"fullUrl": "Endpoint/ep-1",
10+
"resource": {
11+
"resourceType": "Endpoint",
12+
"id": "ep-1",
13+
"meta": {
14+
"versionId": "1",
15+
"lastUpdated": "2025-08-01T14:29:31.987+00:00",
16+
"profile": [
17+
"http://nuts-foundation.github.io/nl-generic-functions-ig/StructureDefinition/nl-gf-endpoint"
18+
]
19+
},
20+
"status": "active",
21+
"connectionType": {
22+
"system": "http://fhir.nl/fhir/NamingSystem/endpoint-connection-type",
23+
"code": "http"
24+
},
25+
"name": "FHIR",
26+
"managingOrganization": {
27+
"reference": "Organization/org-1"
28+
},
29+
"period": {
30+
"start": "2025-01-01"
31+
},
32+
"address": "http://example.com/fhir"
33+
}
34+
}
35+
]
36+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"resourceType": "Bundle",
3+
"id": "84b66851-14f9-4c9d-b79d-1d20620ceaa7",
4+
"meta": {
5+
"lastUpdated": "2025-09-11T13:21:12.499+00:00"
6+
},
7+
"type": "history",
8+
"total": 0,
9+
"entry": []
10+
}

component/mcsdadmin/templates/organization_endpoints.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{{define "main"}}
22
<div id="edit-associations">
3-
<h3>Organization: Care4U Hospital</h3>
3+
<h3>Organization: {{ .Organization.Name }}</h3>
44
<label>Endpoints:</label>
55
<div id="endpoints">
66
{{ range .EndpointCards }}

docs/INTEGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Set the `notify_endpoint` in your Knooppunt configuration (`knooppunt.yml`):
181181

182182
```yaml
183183
mitz:
184-
mitzbase: "https://tst-api.mijn-mitz.nl"
184+
mitzbase: "https://tst-api.mijn-mitz.nl/tst-us/mitz"
185185
notify_endpoint: "https://your-platform.example.com/mitz/notify"
186186
# ... other MITZ settings
187187
```

docs/auth-sd.puml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@startuml
2+
3+
title Authentication Sequence Diagram
4+
5+
actor User
6+
participant Browser
7+
participant EHR
8+
participant Knooppunt
9+
10+
participant RemoteXIS as "Remote XIS"
11+
12+
User -> EHR : Interact
13+
group User AuthN - OpenID Authorized Code\n(e.g. EHR login)
14+
EHR -> EHR : Make Auth Request\nURL
15+
EHR -> Browser : Redirect\n/Open
16+
Browser -> Knooppunt : OIDC Authorization Request\n(scope=openid profile)
17+
18+
group GF Auth (omitted for brevity)
19+
Knooppunt --> Browser : Request authentication
20+
Browser -> Knooppunt : Submit auth credential
21+
end
22+
23+
Knooppunt --> Browser : OIDC Auth Response\n(auth code)
24+
Browser -> EHR : Redirect\n(auth code)
25+
EHR -> Knooppunt : OIDC Token Request\n(auth code)
26+
Knooppunt --> EHR : OIDC Token Response\n(id_token)
27+
end
28+
29+
group User data request
30+
User -> EHR : Request data\nfrom remote source
31+
group OAuth2 Client Credentials
32+
EHR -> Knooppunt : OAuth2 Token Request\n(id_token)
33+
Knooppunt --> EHR : OAuth2 Token Response\n(access token)
34+
end
35+
36+
EHR -> RemoteXIS : Data request\n(access token)
37+
RemoteXIS --> EHR : Data response\n(e.g. FHIR)
38+
EHR --> User : Data response
39+
end
40+
41+
group Non-user data request
42+
EHR -> EHR : System-initiated\ndata request
43+
group OAuth2 Client Credentials
44+
EHR -> Knooppunt : OAuth2 Token Request
45+
Knooppunt --> EHR : OAuth2 Token Response\n(access token)
46+
end
47+
48+
EHR -> RemoteXIS : Data request\n(access token)
49+
RemoteXIS --> EHR : Data response\n(e.g. FHIR)
50+
EHR -> EHR : Process data
51+
end
52+
53+
@enduml

docs/generate.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ docker run --rm -v "${PWD}:/docs:ro" -v "${PWD}/images_im:/diagrams" \
1818
# Post-processing: convert generated PlantUML files to SVG using PlantUML Docker image.
1919
# This will look for files with .puml or .plantuml extensions under docs/diagrams
2020
# and run PlantUML to produce .svg files alongside them.
21-
21+
cp "${PWD}/"*.puml "${PWD}/images_im/"
2222
docker run --rm -v "${PWD}/images_im:/diagrams:ro" -v "${PWD}/images:/images" plantuml/plantuml:latest \
2323
plantuml -verbose -tsvg -o /images /diagrams

0 commit comments

Comments
 (0)