Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions ccsapi/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,41 @@ func TestGetService_Success(t *testing.T) {
assert.Equal(t, "default", resp.DefaultOidcScope)
}

func TestGetService_TrustedIssuersListsSerializeAsPlainURLs(t *testing.T) {
repo := &mockServiceRepository{
getServiceFn: func(_ context.Context, id string) (config.ConfiguredService, error) {
return config.ConfiguredService{
Id: id,
DefaultOidcScope: "default",
ServiceScopes: map[string]config.ScopeEntry{
"default": {
Credentials: []config.Credential{{
Type: "VerifiableCredential",
TrustedIssuersLists: config.TrustedIssuersLists{
{Type: "ebsi-v5", Url: "https://til-v5.example.com"},
{Type: "ebsi", Url: "https://til-v3.example.com"},
},
}},
},
},
}, nil
},
}

w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/service/my-service", nil)
setupRouter(repo).ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)

var raw map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
scopes := raw["oidcScopes"].(map[string]interface{})
credentials := scopes["default"].(map[string]interface{})["credentials"].([]interface{})
trustedIssuersLists := credentials[0].(map[string]interface{})["trustedIssuersLists"]
assert.Equal(t, []interface{}{"https://til-v5.example.com", "https://til-v3.example.com"}, trustedIssuersLists)
}

func TestGetService_NotFound(t *testing.T) {
repo := &mockServiceRepository{
getServiceFn: func(_ context.Context, _ string) (config.ConfiguredService, error) {
Expand Down
16 changes: 16 additions & 0 deletions config/configClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,22 @@ func (t *TrustedIssuersLists) UnmarshalJSON(data []byte) error {
return nil
}

// MarshalJSON always serializes as a plain array of URL strings, dropping the
// per-entry Type. This is the API-facing representation: config.Credential
// (and thus TrustedIssuersLists) is embedded directly in ccsapi request/response
// bodies, and callers only need the endpoint URLs. Type is an internal-only
// concern (dispatches between ebsi/ebsi-v5/gaia-x lookups, see
// trustedparticipant.go) that is never surfaced over the API and is untouched
// on the database persistence path, which round-trips through the separate
// CredentialDB model instead of this type's JSON methods.
func (t TrustedIssuersLists) MarshalJSON() ([]byte, error) {
urls := make([]string, len(t))
for i, entry := range t {
urls[i] = entry.Url
}
return json.Marshal(urls)
}

// trustedIssuersListsType is the reflect.Type for TrustedIssuersLists, cached
// to avoid repeated reflect calls in the decode hook.
var trustedIssuersListsType = reflect.TypeOf(TrustedIssuersLists{})
Expand Down
50 changes: 50 additions & 0 deletions config/configClient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,56 @@ func TestTrustedIssuersLists_UnmarshalJSON(t *testing.T) {
}
}

func TestTrustedIssuersLists_MarshalJSON(t *testing.T) {
type testCase struct {
name string
input TrustedIssuersLists
expected string
}

tests := []testCase{
{
name: "mixed types collapse to plain URLs",
input: TrustedIssuersLists{
{Type: "ebsi-v5", Url: "https://v5.example.com"},
{Type: "ebsi", Url: "https://v3.example.com"},
},
expected: `["https://v5.example.com","https://v3.example.com"]`,
},
{
name: "empty list",
input: TrustedIssuersLists{},
expected: `[]`,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := json.Marshal(tc.input)
assert.NoError(t, err)
assert.JSONEq(t, tc.expected, string(got))
})
}
}

// TestTrustedIssuersLists_RoundTripAsymmetry documents that the API-facing
// serialization is intentionally asymmetric: reading (UnmarshalJSON) still
// accepts the structured {type,url} format so non-default types can be
// configured, but writing (MarshalJSON) always collapses to plain URLs.
// Keeping Unmarshal permissive avoids a breaking change for existing callers
// that send the structured format.
func TestTrustedIssuersLists_RoundTripAsymmetry(t *testing.T) {
input := `[{"type":"ebsi-v5","url":"https://v5.example.com"}]`

var parsed TrustedIssuersLists
assert.NoError(t, json.Unmarshal([]byte(input), &parsed))
assert.Equal(t, "ebsi-v5", parsed[0].Type)

out, err := json.Marshal(parsed)
assert.NoError(t, err)
assert.JSONEq(t, `["https://v5.example.com"]`, string(out))
}

func TestTrustedIssuersListsDecodeHook(t *testing.T) {
hook := TrustedIssuersListsDecodeHook()

Expand Down
Loading