Skip to content

Commit

Permalink
Discovery: HTTP client for communicating with Discovery Service (#2711)
Browse files Browse the repository at this point in the history
* Discovery: HTTP client for communicating with Discovery Service

* import cycle

* shared models package

* pr feedback

* pr feedback
  • Loading branch information
reinkrul authored Jan 9, 2024
1 parent 5b3991b commit 085c43d
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 26 deletions.
1 change: 1 addition & 0 deletions codegen/configs/discovery_v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ output-options:
skip-prune: true
exclude-schemas:
- VerifiablePresentation
- PresentationsResponse
93 changes: 93 additions & 0 deletions discovery/api/v1/client/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package client

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/discovery/api/v1/model"
"io"
"net/http"
"net/url"
"time"
)

// New creates a new DefaultHTTPClient.
func New(strictMode bool, timeout time.Duration, tlsConfig *tls.Config) *DefaultHTTPClient {
return &DefaultHTTPClient{
client: core.NewStrictHTTPClient(strictMode, timeout, tlsConfig),
}
}

var _ HTTPClient = &DefaultHTTPClient{}

// DefaultHTTPClient implements HTTPClient using HTTP.
type DefaultHTTPClient struct {
client core.HTTPRequestDoer
}

func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL string, presentation vc.VerifiablePresentation) error {
requestBody, _ := json.Marshal(presentation)
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, serviceEndpointURL, bytes.NewReader(requestBody))
if err != nil {
return err
}
httpRequest.Header.Set("Content-Type", "application/json")
httpResponse, err := h.client.Do(httpRequest)
if err != nil {
return fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
defer httpResponse.Body.Close()
if err := core.TestResponseCode(201, httpResponse); err != nil {
return fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
return nil
}

func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, tag *string) ([]vc.VerifiablePresentation, *string, error) {
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceEndpointURL, nil)
if tag != nil {
httpRequest.URL.RawQuery = url.Values{"tag": []string{*tag}}.Encode()
}
if err != nil {
return nil, nil, err
}
httpResponse, err := h.client.Do(httpRequest)
if err != nil {
return nil, nil, fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
defer httpResponse.Body.Close()
if err := core.TestResponseCode(200, httpResponse); err != nil {
return nil, nil, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
responseData, err := io.ReadAll(httpResponse.Body)
if err != nil {
return nil, nil, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
var result model.PresentationsResponse
if err := json.Unmarshal(responseData, &result); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
return result.Entries, &result.Tag, nil
}
118 changes: 118 additions & 0 deletions discovery/api/v1/client/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package client

import (
"context"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
testHTTP "github.com/nuts-foundation/nuts-node/test/http"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestHTTPInvoker_Register(t *testing.T) {
vp := vc.VerifiablePresentation{
Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")},
}
vpData, _ := vp.MarshalJSON()
t.Run("ok", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusCreated}
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

err := client.Register(context.Background(), server.URL, vp)

assert.NoError(t, err)
assert.Equal(t, http.MethodPost, handler.Request.Method)
assert.Equal(t, "application/json", handler.Request.Header.Get("Content-Type"))
assert.Equal(t, vpData, handler.RequestData)
})
t.Run("non-ok", func(t *testing.T) {
server := httptest.NewServer(&testHTTP.Handler{StatusCode: http.StatusInternalServerError})
client := New(false, time.Minute, server.TLS)

err := client.Register(context.Background(), server.URL, vp)

assert.ErrorContains(t, err, "non-OK response from remote Discovery Service")
})
}

func TestHTTPInvoker_Get(t *testing.T) {
vp := vc.VerifiablePresentation{
Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")},
}
const clientTag = "client-tag"
const serverTag = "server-tag"
t.Run("no tag from client", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
handler.ResponseData = map[string]interface{}{
"entries": []interface{}{vp},
"tag": serverTag,
}
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

presentations, tag, err := client.Get(context.Background(), server.URL, nil)

assert.NoError(t, err)
assert.Len(t, presentations, 1)
assert.Empty(t, handler.RequestQuery.Get("tag"))
assert.Equal(t, serverTag, *tag)
})
t.Run("tag provided by client", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
handler.ResponseData = map[string]interface{}{
"entries": []interface{}{vp},
"tag": serverTag,
}
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

inputTag := clientTag
presentations, tag, err := client.Get(context.Background(), server.URL, &inputTag)

assert.NoError(t, err)
assert.Len(t, presentations, 1)
assert.Equal(t, clientTag, handler.RequestQuery.Get("tag"))
assert.Equal(t, serverTag, *tag)
})
t.Run("server returns invalid status code", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusInternalServerError}
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

_, _, err := client.Get(context.Background(), server.URL, nil)

assert.ErrorContains(t, err, "non-OK response from remote Discovery Service")
})
t.Run("server does not return JSON", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
handler.ResponseData = "not json"
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

_, _, err := client.Get(context.Background(), server.URL, nil)

assert.ErrorContains(t, err, "failed to unmarshal response from remote Discovery Service")
})
}
35 changes: 35 additions & 0 deletions discovery/api/v1/client/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package client

import (
"context"
"github.com/nuts-foundation/go-did/vc"
)

// HTTPClient is the interface for the client that invokes the remote Discovery Service.
type HTTPClient interface {
// Register registers a Verifiable Presentation on the remote Discovery Service.
Register(ctx context.Context, serviceEndpointURL string, presentation vc.VerifiablePresentation) error

// Get retrieves Verifiable Presentations from the remote Discovery Service, that were added since the given tag.
// If the call succeeds it returns the Verifiable Presentations and the tag that was returned by the server.
// If tag is nil, all Verifiable Presentations are retrieved.
Get(ctx context.Context, serviceEndpointURL string, tag *string) ([]vc.VerifiablePresentation, *string, error)
}
71 changes: 71 additions & 0 deletions discovery/api/v1/client/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 5 additions & 14 deletions discovery/api/v1/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions discovery/api/v1/model/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package model

import "github.com/nuts-foundation/go-did/vc"

// PresentationsResponse is the response for the GetPresentations endpoint.
type PresentationsResponse struct {
Entries []vc.VerifiablePresentation `json:"entries"`
Tag string `json:"tag"`
}
Loading

0 comments on commit 085c43d

Please sign in to comment.