Skip to content

Commit

Permalink
Change tag in discovery service to simple lamport clock value (int) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst authored May 13, 2024
1 parent 4d0d32e commit c939dc1
Show file tree
Hide file tree
Showing 19 changed files with 509 additions and 526 deletions.
33 changes: 20 additions & 13 deletions discovery/api/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ type Wrapper struct {

func (w *Wrapper) ResolveStatusCode(err error) int {
switch {
case errors.Is(err, discovery.ErrServerModeDisabled):
return http.StatusBadRequest
case errors.Is(err, discovery.ErrInvalidPresentation):
return http.StatusBadRequest
default:
Expand All @@ -63,27 +61,36 @@ func (w *Wrapper) Routes(router core.EchoRouter) {
}))
}

func (w *Wrapper) GetPresentations(_ context.Context, request GetPresentationsRequestObject) (GetPresentationsResponseObject, error) {
var tag *discovery.Tag
if request.Params.Tag != nil {
// *string to *Tag
tag = new(discovery.Tag)
*tag = discovery.Tag(*request.Params.Tag)
func (w *Wrapper) GetPresentations(ctx context.Context, request GetPresentationsRequestObject) (GetPresentationsResponseObject, error) {
var timestamp int
if request.Params.Timestamp != nil {
timestamp = *request.Params.Timestamp
}
presentations, newTag, err := w.Server.Get(request.ServiceID, tag)

presentations, newTimestamp, err := w.Server.Get(contextWithForwardedHost(ctx), request.ServiceID, timestamp)
if err != nil {
return nil, err
}
return GetPresentations200JSONResponse{
Entries: presentations,
Tag: string(*newTag),
Entries: presentations,
Timestamp: newTimestamp,
}, nil
}

func (w *Wrapper) RegisterPresentation(_ context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) {
err := w.Server.Register(request.ServiceID, *request.Body)
func (w *Wrapper) RegisterPresentation(ctx context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) {
err := w.Server.Register(contextWithForwardedHost(ctx), request.ServiceID, *request.Body)
if err != nil {
return nil, err
}
return RegisterPresentation201Response{}, nil
}

func contextWithForwardedHost(ctx context.Context) context.Context {
// cast context to echo.Context
echoCtx := ctx.Value("echo.Context")
if echoCtx != nil {
// forward X-Forwarded-Host header via context
ctx = context.WithValue(ctx, discovery.XForwardedHostContextKey{}, echoCtx.(echo.Context).Request().Header.Get("X-Forwarded-Host"))
}
return ctx
}
45 changes: 21 additions & 24 deletions discovery/api/server/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
package server

import (
"context"
"errors"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/discovery"
"github.com/stretchr/testify/assert"
Expand All @@ -32,58 +32,56 @@ import (

const serviceID = "wonderland"

var subjectDID = did.MustParseDID("did:web:example.com")

func TestWrapper_GetPresentations(t *testing.T) {
t.Run("no tag", func(t *testing.T) {
latestTag := discovery.Tag("latest")
lastTimestamp := 1
presentations := map[string]vc.VerifiablePresentation{}
ctx := context.Background()
t.Run("no timestamp", func(t *testing.T) {
test := newMockContext(t)
presentations := []vc.VerifiablePresentation{}
test.server.EXPECT().Get(serviceID, nil).Return(presentations, &latestTag, nil)
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(presentations, lastTimestamp, nil)

response, err := test.wrapper.GetPresentations(nil, GetPresentationsRequestObject{ServiceID: serviceID})
response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID})

require.NoError(t, err)
require.IsType(t, GetPresentations200JSONResponse{}, response)
assert.Equal(t, latestTag, discovery.Tag(response.(GetPresentations200JSONResponse).Tag))
assert.Equal(t, lastTimestamp, response.(GetPresentations200JSONResponse).Timestamp)
assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries)
})
t.Run("with tag", func(t *testing.T) {
givenTag := discovery.Tag("given")
latestTag := discovery.Tag("latest")
t.Run("with timestamp", func(t *testing.T) {
givenTimestamp := 1
test := newMockContext(t)
presentations := []vc.VerifiablePresentation{}
test.server.EXPECT().Get(serviceID, &givenTag).Return(presentations, &latestTag, nil)
test.server.EXPECT().Get(gomock.Any(), serviceID, 1).Return(presentations, lastTimestamp, nil)

response, err := test.wrapper.GetPresentations(nil, GetPresentationsRequestObject{
response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{
ServiceID: serviceID,
Params: GetPresentationsParams{
Tag: (*string)(&givenTag),
Timestamp: &givenTimestamp,
},
})

require.NoError(t, err)
require.IsType(t, GetPresentations200JSONResponse{}, response)
assert.Equal(t, latestTag, discovery.Tag(response.(GetPresentations200JSONResponse).Tag))
assert.Equal(t, lastTimestamp, response.(GetPresentations200JSONResponse).Timestamp)
assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries)
})
t.Run("error", func(t *testing.T) {
test := newMockContext(t)
test.server.EXPECT().Get(serviceID, nil).Return(nil, nil, errors.New("foo"))
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(nil, 0, errors.New("foo"))

_, err := test.wrapper.GetPresentations(nil, GetPresentationsRequestObject{ServiceID: serviceID})
_, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID})

assert.Error(t, err)
})
}

func TestWrapper_RegisterPresentation(t *testing.T) {
ctx := context.Background()
t.Run("ok", func(t *testing.T) {
test := newMockContext(t)
presentation := vc.VerifiablePresentation{}
test.server.EXPECT().Register(serviceID, presentation).Return(nil)
test.server.EXPECT().Register(gomock.Any(), serviceID, presentation).Return(nil)

response, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{
response, err := test.wrapper.RegisterPresentation(ctx, RegisterPresentationRequestObject{
ServiceID: serviceID,
Body: &presentation,
})
Expand All @@ -94,9 +92,9 @@ func TestWrapper_RegisterPresentation(t *testing.T) {
t.Run("error", func(t *testing.T) {
test := newMockContext(t)
presentation := vc.VerifiablePresentation{}
test.server.EXPECT().Register(serviceID, presentation).Return(discovery.ErrInvalidPresentation)
test.server.EXPECT().Register(gomock.Any(), serviceID, presentation).Return(discovery.ErrInvalidPresentation)

_, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{
_, err := test.wrapper.RegisterPresentation(ctx, RegisterPresentationRequestObject{
ServiceID: serviceID,
Body: &presentation,
})
Expand All @@ -107,7 +105,6 @@ func TestWrapper_RegisterPresentation(t *testing.T) {

func TestWrapper_ResolveStatusCode(t *testing.T) {
expected := map[error]int{
discovery.ErrServerModeDisabled: http.StatusBadRequest,
discovery.ErrInvalidPresentation: http.StatusBadRequest,
errors.New("foo"): http.StatusInternalServerError,
}
Expand Down
20 changes: 10 additions & 10 deletions discovery/api/server/client/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL stri
return err
}
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("X-Forwarded-Host", httpRequest.Host) // prevent cycles
httpResponse, err := h.client.Do(httpRequest)
if err != nil {
return fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
Expand All @@ -65,29 +66,28 @@ func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL stri
return nil
}

func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, tag string) ([]vc.VerifiablePresentation, string, error) {
func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error) {
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceEndpointURL, nil)
if tag != "" {
httpRequest.URL.RawQuery = url.Values{"tag": []string{tag}}.Encode()
}
httpRequest.URL.RawQuery = url.Values{"timestamp": []string{fmt.Sprintf("%d", timestamp)}}.Encode()
if err != nil {
return nil, "", err
return nil, 0, err
}
httpRequest.Header.Set("X-Forwarded-Host", httpRequest.Host) // prevent cycles
httpResponse, err := h.client.Do(httpRequest)
if err != nil {
return nil, "", fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
return nil, 0, 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, "", fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
return nil, 0, 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, "", fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
return nil, 0, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
var result PresentationsResponse
if err := json.Unmarshal(responseData, &result); err != nil {
return nil, "", fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
return nil, 0, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
}
return result.Entries, result.Tag, nil
return result.Entries, result.Timestamp, nil
}
49 changes: 33 additions & 16 deletions discovery/api/server/client/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"github.com/nuts-foundation/go-did/vc"
testHTTP "github.com/nuts-foundation/nuts-node/test/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -61,46 +63,61 @@ 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) {

t.Run("no timestamp from client", func(t *testing.T) {
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
handler.ResponseData = map[string]interface{}{
"entries": []interface{}{vp},
"tag": serverTag,
"entries": map[string]interface{}{"1": vp},
"timestamp": 1,
}
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

presentations, tag, err := client.Get(context.Background(), server.URL, "")
presentations, timestamp, err := client.Get(context.Background(), server.URL, 0)

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

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

assert.NoError(t, err)
assert.Len(t, presentations, 1)
assert.Equal(t, clientTag, handler.RequestQuery.Get("tag"))
assert.Equal(t, serverTag, tag)
assert.Equal(t, "1", handler.RequestQuery.Get("timestamp"))
assert.Equal(t, 1, timestamp)
})
t.Run("check X-Forwarded-Host header", func(t *testing.T) {
// custom handler to check the X-Forwarded-Host header
var capturedRequest *http.Request
handler := func(writer http.ResponseWriter, request *http.Request) {
capturedRequest = request
writer.WriteHeader(http.StatusOK)
writer.Write([]byte("{}"))
}
server := httptest.NewServer(http.HandlerFunc(handler))
client := New(false, time.Minute, server.TLS)

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

require.NoError(t, err)
assert.True(t, strings.HasPrefix(capturedRequest.Header.Get("X-Forwarded-Host"), "127.0.0.1"))
})
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, "")
_, _, err := client.Get(context.Background(), server.URL, 0)

assert.ErrorContains(t, err, "non-OK response from remote Discovery Service")
})
Expand All @@ -110,7 +127,7 @@ func TestHTTPInvoker_Get(t *testing.T) {
server := httptest.NewServer(handler)
client := New(false, time.Minute, server.TLS)

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

assert.ErrorContains(t, err, "failed to unmarshal response from remote Discovery Service")
})
Expand Down
8 changes: 4 additions & 4 deletions discovery/api/server/client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ 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 empty, all Verifiable Presentations are retrieved.
Get(ctx context.Context, serviceEndpointURL string, tag string) ([]vc.VerifiablePresentation, string, error)
// Get retrieves Verifiable Presentations from the remote Discovery Service, that were added since the given timestamp.
// If the call succeeds it returns the Verifiable Presentations and the timestamp that was returned by the server.
// If the given timestamp is 0, all Verifiable Presentations are retrieved.
Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error)
}
12 changes: 6 additions & 6 deletions discovery/api/server/client/mock.go

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

6 changes: 4 additions & 2 deletions discovery/api/server/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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"`
// Entries contains mappings from timestamp (as string) to a VerifiablePresentation.
Entries map[string]vc.VerifiablePresentation `json:"entries"`
// Timestamp is the timestamp of the latest entry. It's not a unix timestamp but a Lamport Clock.
Timestamp int `json:"timestamp"`
}
12 changes: 6 additions & 6 deletions discovery/api/server/generated.go

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

Loading

0 comments on commit c939dc1

Please sign in to comment.