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",