Skip to content

Change tag in discovery service to simple lamport clock value (int) #3098

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

Merged
merged 13 commits into from
May 13, 2024
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