From 0cbbe9324ba2c17ab535f0d2afc9c577b740b119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Franke?= Date: Tue, 10 Dec 2024 15:10:04 +0100 Subject: [PATCH] Add tests for every contract endpoint. This adds tests for every contract HTTP endpoint on both the provider and consumer side. These tests are based on the examples in the [IDSA dataspace specification](https://docs.internationaldataspaces.org/ids-knowledgebase/dataspace-protocol/contract-negotiation/contract.negotiation.protocol). The average test goes as follows: 1. Create a starting state. 2. Submit the message to the correct endpoint. 3. Inspect if the response is correct. 4. Inspect if the storage is in the desired state. 5. Inspect if the right request will be sent out to the reconciliation loop. These tests also exposed some small bugs that are now fixed: - The reconciler is now an interface to make testing possible. - SetConsumerPID now actually sets the consumer PID. - Unify the termination handler as they are on the same path for both the consumer and provider flows. - Fix some ODRL validators. - Fix the JSON-LD unmarshaller, as it put a nil where it shouldn't be. --- dsp/common_handlers.go | 2 +- dsp/contract/negotiation.go | 2 +- dsp/contract_handlers.go | 23 +- dsp/contract_handlers_test.go | 636 ++++++++++++++++++ dsp/control/control.go | 4 +- dsp/routing.go | 7 +- dsp/statemachine/contract_messages.go | 14 +- dsp/statemachine/contract_transitions.go | 8 +- dsp/statemachine/reconciler.go | 33 +- dsp/statemachine/transfer_messages.go | 2 +- .../transfer_request_transitions.go | 4 +- go.mod | 2 +- go.sum | 10 +- jsonld/context.go | 9 +- logging/logger.go | 4 +- odrl/types.go | 8 +- odrl/validators.go | 1 + 17 files changed, 704 insertions(+), 65 deletions(-) create mode 100644 dsp/contract_handlers_test.go diff --git a/dsp/common_handlers.go b/dsp/common_handlers.go index 751cbea..3c5c8fc 100644 --- a/dsp/common_handlers.go +++ b/dsp/common_handlers.go @@ -32,7 +32,7 @@ import ( type dspHandlers struct { store persistence.StorageProvider provider providerv1.ProviderServiceClient - reconciler *statemachine.Reconciler + reconciler statemachine.Reconciler selfURL *url.URL dataserviceID string dataserviceEndpoint string diff --git a/dsp/contract/negotiation.go b/dsp/contract/negotiation.go index 8b6b7e8..d2aca16 100644 --- a/dsp/contract/negotiation.go +++ b/dsp/contract/negotiation.go @@ -150,7 +150,7 @@ func (cn *Negotiation) SetProviderPID(u uuid.UUID) { func (cn *Negotiation) SetConsumerPID(u uuid.UUID) { cn.panicRO() - cn.providerPID = u + cn.consumerPID = u cn.modify() } diff --git a/dsp/contract_handlers.go b/dsp/contract_handlers.go index 2c97659..ab31eb9 100644 --- a/dsp/contract_handlers.go +++ b/dsp/contract_handlers.go @@ -245,11 +245,22 @@ func (dh *dspHandlers) providerContractVerificationHandler(w http.ResponseWriter ) } -func (dh *dspHandlers) providerContractTerminationHandler(w http.ResponseWriter, req *http.Request) error { +func (dh *dspHandlers) contractTerminationHandler(w http.ResponseWriter, req *http.Request) error { ctx, _ := logging.InjectLabels(req.Context(), "handler", "providerContractVerificationHandler") req = req.WithContext(ctx) + pid := req.PathValue("PID") + id, err := uuid.Parse(pid) + if err != nil { + return contractError(fmt.Sprintf("Incalid PID: %s", pid), + http.StatusBadRequest, "400", "Invalid request: PID is not a UUID", nil) + } + if _, err := dh.store.GetContractR(ctx, id, constants.DataspaceProvider); err == nil { + return progressContractState[shared.ContractNegotiationTerminationMessage]( + dh, w, req, constants.DataspaceProvider, pid, + ) + } return progressContractState[shared.ContractNegotiationTerminationMessage]( - dh, w, req, constants.DataspaceProvider, req.PathValue("providerPID"), + dh, w, req, constants.DataspaceConsumer, pid, ) } @@ -320,11 +331,3 @@ func (dh *dspHandlers) consumerContractEventHandler(w http.ResponseWriter, req * dh, w, req, constants.DataspaceConsumer, req.PathValue("consumerPID"), ) } - -func (dh *dspHandlers) consumerContractTerminationHandler(w http.ResponseWriter, req *http.Request) error { - ctx, _ := logging.InjectLabels(req.Context(), "handler", "consumerContractEventHandler") - req = req.WithContext(ctx) - return progressContractState[shared.ContractNegotiationTerminationMessage]( - dh, w, req, constants.DataspaceConsumer, req.PathValue("consumerPID"), - ) -} diff --git a/dsp/contract_handlers_test.go b/dsp/contract_handlers_test.go new file mode 100644 index 0000000..57ea859 --- /dev/null +++ b/dsp/contract_handlers_test.go @@ -0,0 +1,636 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "path" + "testing" + "time" + + "github.com/go-dataspace/run-dsp/dsp" + "github.com/go-dataspace/run-dsp/dsp/constants" + "github.com/go-dataspace/run-dsp/dsp/contract" + "github.com/go-dataspace/run-dsp/dsp/persistence" + "github.com/go-dataspace/run-dsp/dsp/persistence/badger" + "github.com/go-dataspace/run-dsp/dsp/shared" + "github.com/go-dataspace/run-dsp/dsp/statemachine" + mockprovider "github.com/go-dataspace/run-dsp/mocks/github.com/go-dataspace/run-dsrpc/gen/go/dsp/v1alpha1" + "github.com/go-dataspace/run-dsp/odrl" + provider "github.com/go-dataspace/run-dsrpc/gen/go/dsp/v1alpha1" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const ( + dataserviceID = "testID" + dataserviceURL = "http://example.com" +) + +var ( + staticProviderPID = uuid.MustParse("42e3656b-751c-40e1-a59c-3a07ec047c01") + staticConsumerPID = uuid.MustParse("435b1eb7-824a-4a88-8dd3-9034b65db45c") + targetID = uuid.MustParse("271d90b7-80ed-4f02-856d-5a881efba4ec") + agreementID = uuid.MustParse("e76c567b-963a-40f4-ad16-e7d88884d880") + odrlOffer = odrl.Offer{ + MessageOffer: odrl.MessageOffer{ + Type: "odrl:Offer", + PolicyClass: odrl.PolicyClass{ + ID: uuid.MustParse("4e3770fd-63d5-4cd7-bb82-bca2ce0cf563").URN(), + AbstractPolicyRule: odrl.AbstractPolicyRule{ + Assigner: "urn:blablabla", + }, + Permission: []odrl.Permission{ + { + Action: "odrl:use", + }, + }, + }, + Target: targetID.URN(), + }, + } + odrlAgreement = odrl.Agreement{ + Type: "odrl:Agreement", + ID: agreementID.URN(), + Target: targetID.URN(), + Timestamp: time.Date(1974, time.September, 9, 13, 14, 15, 0, time.UTC), + PolicyClass: odrl.PolicyClass{}, + } + callBack = shared.MustParseURL("http://example.com") + selfURL = shared.MustParseURL("http://example.org") + pidMap = map[constants.DataspaceRole]uuid.UUID{ + constants.DataspaceProvider: staticProviderPID, + constants.DataspaceConsumer: staticConsumerPID, + } +) + +type mockReconciler struct { + e statemachine.ReconciliationEntry +} + +func (mr *mockReconciler) Add(e statemachine.ReconciliationEntry) { + mr.e = e +} + +type environment struct { + server *httptest.Server + provider *mockprovider.MockProviderServiceClient + store *badger.StorageProvider + reconciler *mockReconciler +} + +func setupEnvironment(t *testing.T) ( + context.Context, + context.CancelFunc, + *environment, +) { + ctx, cancel := context.WithCancel(context.Background()) + logger := slog.New(slog.NewJSONHandler(io.Discard, nil)) + slog.SetDefault(logger) + prov := mockprovider.NewMockProviderServiceClient(t) + store, err := badger.New(ctx, true, "") + reconciler := &mockReconciler{} + assert.Nil(t, err) + pingResponse := &provider.PingResponse{ + ProviderName: "bla", + ProviderDescription: "bla", + Authenticated: false, + DataserviceId: dataserviceID, + DataserviceUrl: dataserviceURL, + } + ts := httptest.NewServer(dsp.GetDSPRoutes(prov, store, reconciler, selfURL, pingResponse)) + e := environment{ + server: ts, + provider: prov, + store: store, + reconciler: reconciler, + } + return ctx, cancel, &e +} + +func fetchAndDecode[T any](ctx context.Context, t *testing.T, method, url string, body io.Reader) T { + t.Helper() + req, err := http.NewRequestWithContext(ctx, method, url, body) + assert.Nil(t, err) + resp, err := http.DefaultClient.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + return decode[T](t, resp.Body) +} + +func decode[T any](t *testing.T, body io.Reader) T { + var thing T + err := json.NewDecoder(body).Decode(&thing) + assert.Nil(t, err) + return thing +} + +func encode[T any](t *testing.T, thing T) io.Reader { + t.Helper() + b := &bytes.Buffer{} + err := json.NewEncoder(b).Encode(thing) + assert.Nil(t, err) + return b +} + +func createNegotiation( + ctx context.Context, + t *testing.T, + store persistence.StorageProvider, + state contract.State, + role constants.DataspaceRole, +) { + t.Helper() + providerPID := staticProviderPID + consumerPID := staticConsumerPID + neg := contract.New( + providerPID, + consumerPID, + state, + odrlOffer, + callBack, + selfURL, + role, + ) + err := store.PutContract(ctx, neg) + assert.Nil(t, err) +} + +func TestNegotiationStatus(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + createNegotiation(ctx, t, env.store, contract.States.OFFERED, constants.DataspaceProvider) + u := env.server.URL + fmt.Sprintf("/negotiations/%s", staticProviderPID.String()) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodGet, u, nil) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.OFFERED.String(), status.State) +} + +func TestNegotiationProviderInitialRequest(t *testing.T) { + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + env.provider.On("GetDataset", mock.Anything, &provider.GetDatasetRequest{ + DatasetId: targetID.String(), + }).Return(&provider.GetDatasetResponse{ + Dataset: &provider.Dataset{}, + }, nil) + u := env.server.URL + "/negotiations/request" + + body := encode(t, shared.ContractRequestMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractRequestMessage", + ConsumerPID: staticConsumerPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: callBack.String(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.NotEqual(t, uuid.UUID{}, status.ProviderPID) + assert.Equal(t, contract.States.REQUESTED.String(), status.State) + + providerPID := uuid.MustParse(status.ProviderPID) + negotiation, err := env.store.GetContractR(ctx, providerPID, constants.DataspaceProvider) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, providerPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.REQUESTED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, providerPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceProvider, reconEntry.Role) + assert.Equal(t, contract.States.OFFERED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticConsumerPID.String(), "offers") + assert.Equal(t, cburl, reconEntry.URL) + + expectedOffer := shared.ContractOfferMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractOfferMessage", + ProviderPID: providerPID.URN(), + ConsumerPID: staticConsumerPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: selfURL.String(), + } + receivedOffer := decode[shared.ContractOfferMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, expectedOffer, receivedOffer) +} + +func TestNegotiationProviderRequest(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + createNegotiation(ctx, t, env.store, contract.States.OFFERED, constants.DataspaceProvider) + + u := env.server.URL + "/negotiations/" + staticProviderPID.String() + "/request" + + body := encode(t, shared.ContractRequestMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractRequestMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: callBack.String(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.REQUESTED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticProviderPID, constants.DataspaceProvider) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.REQUESTED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, staticProviderPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceProvider, reconEntry.Role) + assert.Equal(t, contract.States.AGREED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticConsumerPID.String(), "agreement") + assert.Equal(t, cburl, reconEntry.URL) + + msg := decode[shared.ContractAgreementMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, "dspace:ContractAgreementMessage", msg.Type) + assert.Equal(t, staticProviderPID.URN(), msg.ProviderPID) + assert.Equal(t, staticConsumerPID.URN(), msg.ConsumerPID) + assert.Equal(t, selfURL.String(), msg.CallbackAddress) + assert.Equal(t, targetID.URN(), msg.Agreement.Target) + + agreement, err := env.store.GetAgreement(ctx, uuid.MustParse(msg.Agreement.ID)) + assert.Nil(t, err) + assert.Equal(t, targetID.URN(), agreement.Target) +} + +func TestNegotiationProviderEventAccepted(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + createNegotiation(ctx, t, env.store, contract.States.OFFERED, constants.DataspaceProvider) + + u := env.server.URL + "/negotiations/" + staticProviderPID.String() + "/events" + + body := encode(t, shared.ContractNegotiationEventMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractNegotiationEventMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + EventType: contract.States.ACCEPTED.String(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.ACCEPTED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticProviderPID, constants.DataspaceProvider) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.ACCEPTED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, staticProviderPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceProvider, reconEntry.Role) + assert.Equal(t, contract.States.AGREED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticConsumerPID.String(), "agreement") + assert.Equal(t, cburl, reconEntry.URL) + + msg := decode[shared.ContractAgreementMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, "dspace:ContractAgreementMessage", msg.Type) + assert.Equal(t, staticProviderPID.URN(), msg.ProviderPID) + assert.Equal(t, staticConsumerPID.URN(), msg.ConsumerPID) + assert.Equal(t, selfURL.String(), msg.CallbackAddress) + assert.Equal(t, targetID.URN(), msg.Agreement.Target) + + agreement, err := env.store.GetAgreement(ctx, uuid.MustParse(msg.Agreement.ID)) + assert.Nil(t, err) + assert.Equal(t, targetID.URN(), agreement.Target) +} + +func TestNegotiationProviderAgreementVerification(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + createNegotiation(ctx, t, env.store, contract.States.AGREED, constants.DataspaceProvider) + + u := env.server.URL + "/negotiations/" + staticProviderPID.String() + "/agreement/verification" + + body := encode(t, shared.ContractAgreementVerificationMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractAgreementVerificationMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.VERIFIED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticProviderPID, constants.DataspaceProvider) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.VERIFIED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, staticProviderPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceProvider, reconEntry.Role) + assert.Equal(t, contract.States.FINALIZED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticConsumerPID.String(), "events") + assert.Equal(t, cburl, reconEntry.URL) + + msg := decode[shared.ContractNegotiationEventMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, "dspace:ContractNegotiationEventMessage", msg.Type) + assert.Equal(t, staticProviderPID.URN(), msg.ProviderPID) + assert.Equal(t, staticConsumerPID.URN(), msg.ConsumerPID) + assert.Equal(t, contract.States.FINALIZED.String(), msg.EventType) +} + +func TestNegotiationConsumerInitialOffer(t *testing.T) { + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + u := env.server.URL + "/negotiations/offers" + + body := encode(t, shared.ContractOfferMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractOfferMessage", + ProviderPID: staticProviderPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: callBack.String(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.NotEqual(t, uuid.UUID{}, status.ConsumerPID) + assert.Equal(t, contract.States.OFFERED.String(), status.State) + + consumerPID := uuid.MustParse(status.ConsumerPID) + negotiation, err := env.store.GetContractR(ctx, consumerPID, constants.DataspaceConsumer) + assert.Nil(t, err) + assert.Equal(t, consumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.OFFERED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, consumerPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceConsumer, reconEntry.Role) + assert.Equal(t, contract.States.REQUESTED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticProviderPID.String(), "request") + assert.Equal(t, cburl, reconEntry.URL) + + expectedOffer := shared.ContractRequestMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractRequestMessage", + ProviderPID: staticProviderPID.URN(), + ConsumerPID: consumerPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: selfURL.String() + "/callback", + } + receivedOffer := decode[shared.ContractRequestMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, expectedOffer, receivedOffer) +} + +func TestNegotiationConsumerOffer(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + createNegotiation(ctx, t, env.store, contract.States.REQUESTED, constants.DataspaceConsumer) + + u := env.server.URL + "/callback/negotiations/" + staticConsumerPID.String() + "/offers" + + body := encode(t, shared.ContractOfferMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractOfferMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + Offer: odrlOffer.MessageOffer, + CallbackAddress: callBack.String(), + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.OFFERED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticConsumerPID, constants.DataspaceConsumer) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.OFFERED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, staticConsumerPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceConsumer, reconEntry.Role) + assert.Equal(t, contract.States.ACCEPTED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticProviderPID.String(), "events") + assert.Equal(t, cburl, reconEntry.URL) + + msg := decode[shared.ContractNegotiationEventMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, "dspace:ContractNegotiationEventMessage", msg.Type) + assert.Equal(t, staticProviderPID.URN(), msg.ProviderPID) + assert.Equal(t, staticConsumerPID.URN(), msg.ConsumerPID) + assert.Equal(t, contract.States.ACCEPTED.String(), msg.EventType) +} + +func TestNegotiationConsumerAgreement(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + for _, s := range []contract.State{contract.States.REQUESTED, contract.States.ACCEPTED} { + createNegotiation(ctx, t, env.store, s, constants.DataspaceConsumer) + + u := env.server.URL + "/callback/negotiations/" + staticConsumerPID.String() + "/agreement" + + body := encode(t, shared.ContractAgreementMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractAgreementMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + Agreement: odrlAgreement, + CallbackAddress: callBack.String(), + }) + + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.AGREED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticConsumerPID, constants.DataspaceConsumer) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.AGREED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, &odrlAgreement, negotiation.GetAgreement()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, staticConsumerPID, reconEntry.EntityID) + assert.Equal(t, statemachine.ReconciliationContract, reconEntry.Type) + assert.Equal(t, constants.DataspaceConsumer, reconEntry.Role) + assert.Equal(t, contract.States.VERIFIED.String(), reconEntry.TargetState) + assert.Equal(t, http.MethodPost, reconEntry.Method) + + cburl := shared.MustParseURL(callBack.String()) + cburl.Path = path.Join(cburl.Path, "negotiations", staticProviderPID.String(), "agreement/verification") + assert.Equal(t, cburl, reconEntry.URL) + + msg := decode[shared.ContractAgreementVerificationMessage](t, bytes.NewReader(reconEntry.Body)) + assert.EqualValues(t, "dspace:ContractAgreementVerificationMessage", msg.Type) + assert.Equal(t, staticProviderPID.URN(), msg.ProviderPID) + assert.Equal(t, staticConsumerPID.URN(), msg.ConsumerPID) + + agreement, err := env.store.GetAgreement(ctx, agreementID) + assert.Nil(t, err) + assert.Equal(t, targetID.URN(), agreement.Target) + } +} + +func TestNegotiationConsumerEventFinalized(t *testing.T) { + t.Parallel() + ctx, cancel, env := setupEnvironment(t) + defer cancel() + + createNegotiation(ctx, t, env.store, contract.States.VERIFIED, constants.DataspaceConsumer) + + u := env.server.URL + "/callback/negotiations/" + staticConsumerPID.String() + "/events" + + body := encode(t, shared.ContractNegotiationEventMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractNegotiationEventMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + EventType: contract.States.FINALIZED.String(), + }) + + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.FINALIZED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, staticConsumerPID, constants.DataspaceConsumer) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.FINALIZED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + + reconEntry := env.reconciler.e + assert.Equal(t, statemachine.ReconciliationEntry{}, reconEntry) +} + +func TestNegotiationTermination(t *testing.T) { + t.Parallel() + + for _, r := range []constants.DataspaceRole{constants.DataspaceConsumer, constants.DataspaceProvider} { + for _, s := range []contract.State{ + contract.States.REQUESTED, + contract.States.OFFERED, + contract.States.ACCEPTED, + contract.States.AGREED, + contract.States.VERIFIED, + } { + ctx, cancel, env := setupEnvironment(t) + defer cancel() + u := env.server.URL + "/negotiations/" + pidMap[r].String() + "/termination" + createNegotiation(ctx, t, env.store, s, r) + + body := encode(t, shared.ContractNegotiationTerminationMessage{ + Context: shared.GetDSPContext(), + Type: "dspace:ContractNegotiationTerminationMessage", + ConsumerPID: staticConsumerPID.URN(), + ProviderPID: staticProviderPID.URN(), + Code: "some code", + Reason: []shared.Multilanguage{{ + Value: "en", + Language: "test", + }}, + }) + status := fetchAndDecode[shared.ContractNegotiation](ctx, t, http.MethodPost, u, body) + assert.Equal(t, "dspace:ContractNegotiation", status.Type) + assert.Equal(t, staticConsumerPID.URN(), status.ConsumerPID) + assert.Equal(t, staticProviderPID.URN(), status.ProviderPID) + assert.Equal(t, contract.States.TERMINATED.String(), status.State) + + negotiation, err := env.store.GetContractR(ctx, pidMap[r], r) + assert.Nil(t, err) + assert.Equal(t, staticConsumerPID, negotiation.GetConsumerPID()) + assert.Equal(t, staticProviderPID, negotiation.GetProviderPID()) + assert.Equal(t, contract.States.TERMINATED, negotiation.GetState()) + assert.Equal(t, odrlOffer, negotiation.GetOffer()) + assert.Equal(t, callBack.String(), negotiation.GetCallback().String()) + } + } +} diff --git a/dsp/control/control.go b/dsp/control/control.go index 6589939..46b5675 100644 --- a/dsp/control/control.go +++ b/dsp/control/control.go @@ -45,7 +45,7 @@ type Server struct { requester shared.Requester store persistence.StorageProvider - reconciler *statemachine.Reconciler + reconciler statemachine.Reconciler provider dspv1alpha1.ProviderServiceClient selfURL *url.URL } @@ -53,7 +53,7 @@ type Server struct { func New( requester shared.Requester, store persistence.StorageProvider, - reconciler *statemachine.Reconciler, + reconciler statemachine.Reconciler, provider dspv1alpha1.ProviderServiceClient, selfURL *url.URL, ) *Server { diff --git a/dsp/routing.go b/dsp/routing.go index 3c5ec2e..38e55eb 100644 --- a/dsp/routing.go +++ b/dsp/routing.go @@ -37,7 +37,7 @@ func GetWellKnownRoutes() http.Handler { func GetDSPRoutes( provider providerv1.ProviderServiceClient, store persistence.StorageProvider, - reconciler *statemachine.Reconciler, + reconciler statemachine.Reconciler, selfURL *url.URL, pingResponse *providerv1.PingResponse, ) http.Handler { @@ -62,7 +62,6 @@ func GetDSPRoutes( mux.Handle("POST /negotiations/{providerPID}/events", WrapHandlerWithError(ch.providerContractEventHandler)) mux.Handle("POST /negotiations/{providerPID}/agreement/verification", WrapHandlerWithError(ch.providerContractVerificationHandler)) - mux.Handle("POST /negotiations/{providerPID}/termination", WrapHandlerWithError(ch.providerContractTerminationHandler)) // Contract negotiation consumer callbacks) mux.Handle("POST /negotiations/offers", WrapHandlerWithError(ch.consumerContractOfferHandler)) @@ -71,8 +70,8 @@ func GetDSPRoutes( mux.Handle("POST /callback/negotiations/{consumerPID}/agreement", WrapHandlerWithError(ch.consumerContractAgreementHandler)) mux.Handle("POST /callback/negotiations/{consumerPID}/events", WrapHandlerWithError(ch.consumerContractEventHandler)) - mux.Handle("POST /callback/negotiations/{consumerPID}/termination", - WrapHandlerWithError(ch.consumerContractTerminationHandler)) + + mux.Handle("POST /negotiations/{PID}/termination", WrapHandlerWithError(ch.contractTerminationHandler)) // Transfer process endpoints mux.Handle("GET /transfers/{providerPID}", WrapHandlerWithError(ch.providerTransferProcessHandler)) diff --git a/dsp/statemachine/contract_messages.go b/dsp/statemachine/contract_messages.go index 41df246..14a49ed 100644 --- a/dsp/statemachine/contract_messages.go +++ b/dsp/statemachine/contract_messages.go @@ -43,7 +43,7 @@ func makeContractRequestFunction( cu *url.URL, reqBody []byte, destinationState contract.State, - reconciler *Reconciler, + reconciler Reconciler, ) func() { var id uuid.UUID if c.GetRole() == constants.DataspaceConsumer { @@ -71,7 +71,7 @@ func makeRequestFunction( role constants.DataspaceRole, destinationState string, recType ReconciliationType, - reconciler *Reconciler, + reconciler Reconciler, ) func() { return func() { reconciler.Add(ReconciliationEntry{ @@ -88,7 +88,7 @@ func makeRequestFunction( } //nolint:dupl -func sendContractRequest(ctx context.Context, r *Reconciler, c *contract.Negotiation) (func(), error) { +func sendContractRequest(ctx context.Context, r Reconciler, c *contract.Negotiation) (func(), error) { ctx, logger := logging.InjectLabels(ctx, "operation", "sendContractRequest") contractRequest := shared.ContractRequestMessage{ Context: shared.GetDSPContext(), @@ -127,7 +127,7 @@ func sendContractRequest(ctx context.Context, r *Reconciler, c *contract.Negotia } //nolint:dupl -func sendContractOffer(ctx context.Context, r *Reconciler, c *contract.Negotiation) (func(), error) { +func sendContractOffer(ctx context.Context, r Reconciler, c *contract.Negotiation) (func(), error) { ctx, logger := logging.InjectLabels(ctx, "operation", "sendContractOffer") contractOffer := shared.ContractOfferMessage{ Context: shared.GetDSPContext(), @@ -167,7 +167,7 @@ func sendContractOffer(ctx context.Context, r *Reconciler, c *contract.Negotiati ), nil } -func sendContractAgreement(ctx context.Context, r *Reconciler, c *contract.Negotiation) (func(), error) { +func sendContractAgreement(ctx context.Context, r Reconciler, c *contract.Negotiation) (func(), error) { ctx, logger := logging.InjectLabels(ctx, "operation", "sendContractAgreement") c.SetAgreement(&odrl.Agreement{ PolicyClass: odrl.PolicyClass{}, @@ -204,7 +204,7 @@ func sendContractAgreement(ctx context.Context, r *Reconciler, c *contract.Negot } func sendContractEvent( - ctx context.Context, r *Reconciler, c *contract.Negotiation, pid uuid.UUID, state contract.State, + ctx context.Context, r Reconciler, c *contract.Negotiation, pid uuid.UUID, state contract.State, ) (func(), error) { ctx, logger := logging.InjectLabels(ctx, "operation", "sendContractEvent") contractEvent := shared.ContractNegotiationEventMessage{ @@ -232,7 +232,7 @@ func sendContractEvent( ), nil } -func sendContractVerification(ctx context.Context, r *Reconciler, c *contract.Negotiation) (func(), error) { +func sendContractVerification(ctx context.Context, r Reconciler, c *contract.Negotiation) (func(), error) { ctx, logger := logging.InjectLabels(ctx, "operation", "sendContractVerification") contractVerification := shared.ContractAgreementVerificationMessage{ Context: shared.GetDSPContext(), diff --git a/dsp/statemachine/contract_transitions.go b/dsp/statemachine/contract_transitions.go index 6985e4e..6c0c4ae 100644 --- a/dsp/statemachine/contract_transitions.go +++ b/dsp/statemachine/contract_transitions.go @@ -54,16 +54,16 @@ type ContractNegotiationState interface { Recv(ctx context.Context, message any) (context.Context, ContractNegotiationState, error) Send(ctx context.Context) (func(), error) GetProvider() providerv1.ProviderServiceClient - GetReconciler() *Reconciler + GetReconciler() Reconciler } type stateMachineDeps struct { p providerv1.ProviderServiceClient - r *Reconciler + r Reconciler } func (cd *stateMachineDeps) GetProvider() providerv1.ProviderServiceClient { return cd.p } -func (cd *stateMachineDeps) GetReconciler() *Reconciler { return cd.r } +func (cd *stateMachineDeps) GetReconciler() Reconciler { return cd.r } // ContractNegotiationInitial is an initial state for a contract that hasn't been actually // been submitted yet. @@ -425,7 +425,7 @@ func GetContractNegotiation( ctx context.Context, c *contract.Negotiation, p providerv1.ProviderServiceClient, - r *Reconciler, + r Reconciler, ) (context.Context, ContractNegotiationState) { var cns ContractNegotiationState deps := stateMachineDeps{p: p, r: r} diff --git a/dsp/statemachine/reconciler.go b/dsp/statemachine/reconciler.go index 452361a..33924f8 100644 --- a/dsp/statemachine/reconciler.go +++ b/dsp/statemachine/reconciler.go @@ -78,13 +78,20 @@ type ReconciliationEntry struct { Context context.Context } -// Reconciler tries to send out all the http requests, and retries them if something fails. +// Reconciler is the interface for the reconciler used in the statemachine. It's where +// the statemachine leaves outgoing state machine requests. As all these are done via HTTP +// for now, the only reason for this interface is to allow mocks in testing. +type Reconciler interface { + Add(e ReconciliationEntry) +} + +// HTTPReconciler tries to send out all the http requests, and retries them if something fails. // A request has an exponential backoff that is defined in calculateNextAttempt. // But simply said it takes the previous interval, adds 50% to that, and then randomises it a bit. // // Right now, almost nothing signals an immediate stop, but the option for that is already // available. -type Reconciler struct { +type HTTPReconciler struct { ctx context.Context c chan reconciliationOperation r shared.Requester @@ -97,11 +104,11 @@ type Reconciler struct { sync.Mutex } -func NewReconciler(ctx context.Context, r shared.Requester, s persistence.StorageProvider) *Reconciler { +func NewReconciler(ctx context.Context, r shared.Requester, s persistence.StorageProvider) *HTTPReconciler { q := &deque.Deque[reconciliationOperation]{} q.Grow(initialQueueSize) - return &Reconciler{ + return &HTTPReconciler{ ctx: ctx, c: make(chan reconciliationOperation), r: r, @@ -110,7 +117,7 @@ func NewReconciler(ctx context.Context, r shared.Requester, s persistence.Storag } } -func (r *Reconciler) Run() { +func (r *HTTPReconciler) Run() { r.WaitGroup.Add(1 + workers) go r.manager() for range workers { @@ -118,7 +125,7 @@ func (r *Reconciler) Run() { } } -func (r *Reconciler) Add(entry ReconciliationEntry) { +func (r *HTTPReconciler) Add(entry ReconciliationEntry) { r.Lock() defer r.Unlock() r.q.PushBack(reconciliationOperation{ @@ -130,7 +137,7 @@ func (r *Reconciler) Add(entry ReconciliationEntry) { }) } -func (r *Reconciler) manager() { +func (r *HTTPReconciler) manager() { // We use a ticker to trigger iterations, this is to not hammer the queue in a tight loop. ticker := time.NewTicker(reconciliationMillis * time.Millisecond) logger := logging.Extract(r.ctx) @@ -162,7 +169,7 @@ func (r *Reconciler) manager() { } } -func (r *Reconciler) worker() { +func (r *HTTPReconciler) worker() { // rLogger is the non-entry specific logger for the reconciler rLogger := logging.Extract(r.ctx) rLogger.Info("Starting reconciliation loop") @@ -200,7 +207,7 @@ func (r *Reconciler) worker() { } } -func (r *Reconciler) handleError(ctx context.Context, op reconciliationOperation, err error) { +func (r *HTTPReconciler) handleError(ctx context.Context, op reconciliationOperation, err error) { logger := logging.Extract(ctx).With( "err", err, "submitted", op.Submitted, "attempts", op.Attempts, "orig_next_attempt", op.NextAttempt) // If the error is fatal, just immediately terminate the operation. @@ -220,7 +227,7 @@ func (r *Reconciler) handleError(ctx context.Context, op reconciliationOperation r.Unlock() } -func (r *Reconciler) terminate(ctx context.Context, entry ReconciliationEntry) { +func (r *HTTPReconciler) terminate(ctx context.Context, entry ReconciliationEntry) { logger := logging.Extract(ctx) logger.Error("Terminating entry") @@ -256,7 +263,7 @@ func calculateNextAttempt(currentInterval time.Duration, attempts int) (time.Tim return nextRun, time.Duration(ci) } -func (r *Reconciler) updateState( +func (r *HTTPReconciler) updateState( ctx context.Context, entry ReconciliationEntry, state string, ) error { logger := logging.Extract(ctx) @@ -274,7 +281,7 @@ func (r *Reconciler) updateState( } } -func (c *Reconciler) setTransferState( +func (c *HTTPReconciler) setTransferState( ctx context.Context, state string, role constants.DataspaceRole, id uuid.UUID, ) error { ts, err := transfer.ParseState(state) @@ -296,7 +303,7 @@ func (c *Reconciler) setTransferState( return nil } -func (c *Reconciler) setContractState( +func (c *HTTPReconciler) setContractState( ctx context.Context, state string, role constants.DataspaceRole, id uuid.UUID, ) error { cs, err := contract.ParseState(state) diff --git a/dsp/statemachine/transfer_messages.go b/dsp/statemachine/transfer_messages.go index 0b322e4..3675a04 100644 --- a/dsp/statemachine/transfer_messages.go +++ b/dsp/statemachine/transfer_messages.go @@ -35,7 +35,7 @@ func makeTransferRequestFunction( cu *url.URL, reqBody []byte, destinationState transfer.State, - reconciler *Reconciler, + reconciler Reconciler, ) func() { var id uuid.UUID if t.GetRole() == constants.DataspaceConsumer { diff --git a/dsp/statemachine/transfer_request_transitions.go b/dsp/statemachine/transfer_request_transitions.go index bd77f1d..1fecc06 100644 --- a/dsp/statemachine/transfer_request_transitions.go +++ b/dsp/statemachine/transfer_request_transitions.go @@ -52,7 +52,7 @@ type TransferRequestNegotiationState interface { Recv(ctx context.Context, message any) (TransferRequestNegotiationState, error) Send(ctx context.Context) (func(), error) GetProvider() providerv1.ProviderServiceClient - GetReconciler() *Reconciler + GetReconciler() Reconciler } type TransferRequestNegotiationInitial struct { @@ -226,7 +226,7 @@ func (tr *TransferRequestNegotiationTerminated) Send(ctx context.Context) (func( } func GetTransferRequestNegotiation( - tr *transfer.Request, p providerv1.ProviderServiceClient, r *Reconciler, + tr *transfer.Request, p providerv1.ProviderServiceClient, r Reconciler, ) TransferRequestNegotiationState { deps := stateMachineDeps{p: p, r: r} switch tr.GetState() { diff --git a/go.mod b/go.mod index e226108..3ca99ff 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( github.com/alecthomas/chroma/v2 v2.14.0 + github.com/dgraph-io/badger/v4 v4.4.0 github.com/fatih/color v1.17.0 github.com/gammazero/deque v1.0.0 github.com/go-dataspace/run-dsrpc v0.0.3-alpha1 @@ -23,7 +24,6 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/badger/v4 v4.4.0 // indirect github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index ce796d4..25f6220 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/dgraph-io/badger/v4 v4.4.0 h1:rA48XiDynZLyMdlaJl67p9+lqfqwxlgKtCpYLAi github.com/dgraph-io/badger/v4 v4.4.0/go.mod h1:sONMmPPfbnj9FPwS/etCqky/ULth6CQJuAZSuWCmixE= github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ= github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -168,8 +170,6 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -189,8 +189,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -205,14 +203,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/jsonld/context.go b/jsonld/context.go index da9cee9..b010b82 100644 --- a/jsonld/context.go +++ b/jsonld/context.go @@ -75,24 +75,23 @@ type Context struct { // if that fails, it will try to unmarshal it as a list of strings, and if that fails, as // a single string. func (c *Context) UnmarshalJSON(data []byte) error { + c.namedContexts = make(map[string]ContextEntry) var nc map[string]ContextEntry if err := json.Unmarshal(data, &nc); err == nil { c.namedContexts = nc return nil } - rootContexts := make([]ContextEntry, 0) + c.rootContexts = make([]ContextEntry, 0) var lc []string if err := json.Unmarshal(data, &lc); err == nil { for _, id := range lc { - rootContexts = append(rootContexts, ContextEntry{ID: id}) + c.rootContexts = append(c.rootContexts, ContextEntry{ID: id}) } - c.rootContexts = rootContexts return nil } var sc string if err := json.Unmarshal(data, &sc); err == nil { - rootContexts = append(rootContexts, ContextEntry{ID: sc}) - c.rootContexts = rootContexts + c.rootContexts = append(c.rootContexts, ContextEntry{ID: sc}) return nil } return fmt.Errorf("Couldn't unmarshal Context: %s", data) diff --git a/logging/logger.go b/logging/logger.go index d9a9915..ead5445 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -70,8 +70,8 @@ func Inject(ctx context.Context, logger *slog.Logger) context.Context { func Extract(ctx context.Context) *slog.Logger { ctxVal := ctx.Value(contextKey) if ctxVal == nil { - logger := NewJSON("info", false) - logger.Warn("logger not found in context, returning default logger with level info") + logger := slog.Default() + logger.Debug("logger not found in context, using default logger") return logger } logger, ok := ctxVal.(*slog.Logger) diff --git a/odrl/types.go b/odrl/types.go index 8703150..8eb82b9 100644 --- a/odrl/types.go +++ b/odrl/types.go @@ -58,8 +58,8 @@ type Reference struct { // Permission is a permisson entry. type Permission struct { AbstractPolicyRule - Action string `json:"action" validate:"required,odrl_action"` - Constraint []Constraint `json:"constraint,omitempty" validate:"gte=1,dive"` + Action string `json:"action" validate:"odrl_action"` + Constraint []Constraint `json:"constraint,omitempty" validate:"dive"` Duty Duty `json:"duty,omitempty"` } @@ -67,8 +67,8 @@ type Permission struct { type Duty struct { AbstractPolicyRule ID string `json:"@id,omitempty"` - Action string `json:"action,omitempty" validate:"required,odrl_action"` - Constraint []Constraint `json:"constraint,omitempty" validate:"gte=1,dive"` + Action string `json:"action,omitempty" validate:"odrl_action"` + Constraint []Constraint `json:"constraint,omitempty" validate:"dive"` } // Constraint is an ODRL constraint. diff --git a/odrl/validators.go b/odrl/validators.go index f532f7d..cb32cd6 100644 --- a/odrl/validators.go +++ b/odrl/validators.go @@ -22,6 +22,7 @@ import ( func action(fl validator.FieldLevel) bool { states := []string{ + "", "odrl:delete", "odrl:execute", "cc:SourceCode",