Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discovery: HTTP client for communicating with Discovery Service #2711

Merged
merged 5 commits into from
Jan 9, 2024
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
5 changes: 2 additions & 3 deletions auth/api/iam/generated.go

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

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed when a ReadAll is done?

Copy link
Member Author

@reinkrul reinkrul Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case ReadAll() fails. But then, the connection might not be reused anyways;

        ... It is the caller's responsibility to
	// close Body. The default HTTP client's Transport may not
	// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
	// not read to completion and closed.

I tend to always make sure I close external resources, what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might panic

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)
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
}
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.

Loading
Loading