diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 0b8c0143ec..02e25f9aff 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -388,6 +388,65 @@ jobs: coverage-artifact-name: "coverage_signed_docs" coverage-path: coverage.txt + # These jobs run the networked tests with one node on an older release, to catch + # changes that break compatibility with it. Both directions are run because they + # fail differently: old-source has the older node sending, new-source receiving. + # + # The release binary is a public download over HTTPS, so no token is needed. + test-coverage-cross-version-old-source: + name: Test coverage cross version old source job + + runs-on: runs-on=${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}/\ + spot=pco/cpu=16+32/family=c6*+c7*/disk=large/extras=s3-cache + + env: + DEFRA_MULTIPLIERS: cross-version-old-source + + steps: + - name: Enable RunsOn action + uses: runs-on/action@v2 + with: + metrics: cpu,network,memory,disk,io + + - name: Checkout code into the directory + uses: actions/checkout@v6 + + - name: Setup defradb + uses: ./.github/composites/setup-defradb + + - name: Test coverage & save coverage report in an artifact + uses: ./.github/composites/test-coverage-with-artifact + with: + coverage-artifact-name: "coverage_cross_version_old_source" + coverage-path: coverage.txt + + test-coverage-cross-version-new-source: + name: Test coverage cross version new source job + + runs-on: runs-on=${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}/\ + spot=pco/cpu=16+32/family=c6*+c7*/disk=large/extras=s3-cache + + env: + DEFRA_MULTIPLIERS: cross-version-new-source + + steps: + - name: Enable RunsOn action + uses: runs-on/action@v2 + with: + metrics: cpu,network,memory,disk,io + + - name: Checkout code into the directory + uses: actions/checkout@v6 + + - name: Setup defradb + uses: ./.github/composites/setup-defradb + + - name: Test coverage & save coverage report in an artifact + uses: ./.github/composites/test-coverage-with-artifact + with: + coverage-artifact-name: "coverage_cross_version_new_source" + coverage-path: coverage.txt + # This job tests the leveldb datastore. test-coverage-leveldb: name: Test coverage leveldb job @@ -431,6 +490,8 @@ jobs: - test-coverage-js # 1 test(s) - test-coverage-secondary-index # 1 test(s) - test-coverage-signed-docs # 1 test(s) + - test-coverage-cross-version-old-source # 1 test(s) + - test-coverage-cross-version-new-source # 1 test(s) - test-coverage-leveldb # 1 test(s) # Important to know: diff --git a/tests/action/acp_dac_config.go b/tests/action/acp_dac_config.go new file mode 100644 index 0000000000..94a89735e4 --- /dev/null +++ b/tests/action/acp_dac_config.go @@ -0,0 +1,33 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package action + +import ( + "os" + + "github.com/sourcenetwork/defradb/tests/state" +) + +const documentACPTypeEnvName = "DEFRA_DOCUMENT_ACP_TYPE" + +// DocumentACPType is the document ACP implementation under test. +// +// Node setup and the test harness both read this, so it is resolved once here +// rather than copied into each package. +var DocumentACPType state.DocumentACPType + +func init() { + DocumentACPType = state.DocumentACPType(os.Getenv(documentACPTypeEnvName)) + if DocumentACPType == "" { + DocumentACPType = state.LocalDocumentACPType + } +} diff --git a/tests/action/assert_request.go b/tests/action/assert_request.go index 1af9b0d66d..7349b456c6 100644 --- a/tests/action/assert_request.go +++ b/tests/action/assert_request.go @@ -156,7 +156,7 @@ func assertRequestResults( default: assertResultsEqual( s.T, - s.ClientType, + clientTypeForNode(s, nodeID), expect, actual, fmt.Sprintf("node: %v, path: %s", nodeID, stack), @@ -273,14 +273,14 @@ func assertRequestResultDoc( if ordered { assertResultsEqual( s.T, - s.ClientType, + clientTypeForNode(s, nodeID), expectedDocID, actualValue, fmt.Sprintf("node: %v, path: %s", nodeID, stack), ) } else { ok := isResultsEqual( - s.ClientType, + clientTypeForNode(s, nodeID), expectedDocID, actualValue, ) @@ -323,14 +323,14 @@ func assertRequestResultDoc( if ordered { assertResultsEqual( s.T, - s.ClientType, + clientTypeForNode(s, nodeID), expectedValue, actualValue, fmt.Sprintf("node: %v, path: %s", nodeID, stack), ) } else { ok := isResultsEqual( - s.ClientType, + clientTypeForNode(s, nodeID), expectedValue, actualValue, ) diff --git a/tests/action/eventually.go b/tests/action/eventually.go new file mode 100644 index 0000000000..e0cac8ed31 --- /dev/null +++ b/tests/action/eventually.go @@ -0,0 +1,163 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package action + +import ( + "fmt" + "testing" + "time" +) + +const ( + // eventuallyTimeout is how long [Eventually] retries before giving up. + eventuallyTimeout = 20 * time.Second + // eventuallyInterval is how long [Eventually] waits between attempts. + eventuallyInterval = 100 * time.Millisecond +) + +// Eventually runs another action until it stops failing. +// +// Use it when the harness cannot tell that something has finished and the test +// has to keep asking. The clearest case is a node running an older release: it +// can hold a document written against a schema it has never seen, but it cannot +// report the commit, so there is no signal to wait for. +// +// The wrapped action's assertions are captured rather than failing the test, so +// a failed attempt is just a retry. The final attempt's failure is reported as +// this action's failure. +type Eventually struct { + stateful + + // Action is retried until it passes or the timeout is reached. + Action Action + + // Timeout overrides the default retry window. + Timeout time.Duration +} + +var _ Action = (*Eventually)(nil) +var _ Stateful = (*Eventually)(nil) + +func (a *Eventually) Execute() { + timeout := a.Timeout + if timeout == 0 { + timeout = eventuallyTimeout + } + + realT := a.s.T + // Restore with a defer: a real panic from the nested action skips past the + // assignment below, which would leave the state pointing at a recorder that + // nothing reads, silently swallowing later failures. + defer func() { a.s.T = realT }() + deadline := time.Now().Add(timeout) + + var lastErr string + for { + recorder := &recordingT{TB: realT} + a.s.T = recorder + if stateful, ok := a.Action.(Stateful); ok { + stateful.SetState(a.s) + } + failed := a.attempt(recorder) + a.s.T = realT + + if !failed { + return + } + lastErr = recorder.message + + if time.Now().After(deadline) { + a.s.T.Errorf("action did not pass within %s: %s", timeout, lastErr) + a.s.T.FailNow() + return + } + time.Sleep(eventuallyInterval) + } +} + +// attempt runs the action once, reporting whether it failed rather than failing +// the test. +func (a *Eventually) attempt(recorder *recordingT) (failed bool) { + defer func() { + r := recover() + if r == nil { + return + } + if _, ok := r.(attemptFailure); ok { + failed = true + return + } + // Anything else is a real panic and belongs to the caller. + panic(r) + }() + + a.Action.Execute() + return recorder.failed +} + +// attemptFailure marks an attempt that ended early because an assertion failed. +// +// The failure is raised as a panic rather than by ending the goroutine, so the +// retry loop can recover from it in place. This mirrors how the flake retry +// helper in the integration package handles the same problem. +type attemptFailure struct{} + +// recordingT captures assertion failures instead of failing the test. +type recordingT struct { + testing.TB + + failed bool + message string +} + +func (t *recordingT) Errorf(format string, args ...any) { + t.fail(fmt.Sprintf(format, args...)) +} + +func (t *recordingT) Error(args ...any) { + t.fail(fmt.Sprint(args...)) +} + +func (t *recordingT) Fatal(args ...any) { + t.fail(fmt.Sprint(args...)) + t.FailNow() +} + +func (t *recordingT) Fatalf(format string, args ...any) { + t.fail(fmt.Sprintf(format, args...)) + t.FailNow() +} + +func (t *recordingT) Fail() { + t.fail("") +} + +func (t *recordingT) FailNow() { + t.failed = true + panic(attemptFailure{}) +} + +func (t *recordingT) Failed() bool { + return t.failed +} + +func (t *recordingT) fail(message string) { + t.failed = true + if message != "" { + t.message = message + } +} + +// NewEventually returns an [Eventually] wrapping the given action. +func NewEventually(action Action) *Eventually { + return &Eventually{Action: action} +} diff --git a/tests/action/eventually_test.go b/tests/action/eventually_test.go new file mode 100644 index 0000000000..3ebbec7731 --- /dev/null +++ b/tests/action/eventually_test.go @@ -0,0 +1,168 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package action + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sourcenetwork/defradb/tests/state" +) + +// fakeAction fails a set number of times before passing, recording how often it ran. +type fakeAction struct { + stateful + + failures int + runs int + // panicWith is raised on the first run when set, standing in for a real bug + // in an action rather than a failed assertion. + panicWith string +} + +var _ Action = (*fakeAction)(nil) +var _ Stateful = (*fakeAction)(nil) + +func (a *fakeAction) Execute() { + a.runs++ + if a.panicWith != "" { + panic(a.panicWith) + } + if a.runs <= a.failures { + require.Fail(a.s.T, "not ready yet") + } +} + +func newEventuallyState(t testing.TB) *state.State { + return &state.State{T: t} +} + +func TestEventually_PassesFirstTime_RunsOnce(t *testing.T) { + inner := &fakeAction{} + act := &Eventually{Action: inner} + act.SetState(newEventuallyState(t)) + + act.Execute() + + assert.Equal(t, 1, inner.runs, "a passing action should not be retried") +} + +func TestEventually_PassesAfterRetries_KeepsTrying(t *testing.T) { + inner := &fakeAction{failures: 3} + act := &Eventually{Action: inner} + act.SetState(newEventuallyState(t)) + + act.Execute() + + assert.Equal(t, 4, inner.runs, "should retry until the action passes") +} + +func TestEventually_RestoresTheRealT(t *testing.T) { + // The real T is swapped out while an attempt runs so its failures can be + // captured. It has to be put back, or later actions would report into a + // recorder nobody reads. + s := newEventuallyState(t) + act := &Eventually{Action: &fakeAction{failures: 1}} + act.SetState(s) + + act.Execute() + + assert.Same(t, t, s.T, "the original T must be restored") +} + +func TestEventually_NeverPasses_FailsWithTheLastError(t *testing.T) { + inner := &fakeAction{failures: 1000} + act := &Eventually{Action: inner, Timeout: 300 * time.Millisecond} + + recorder := &recordingT{TB: t} + act.SetState(&state.State{T: recorder}) + + // The final failure ends the run the same way an assertion would, so it has + // to be called on its own goroutine. + done := make(chan struct{}) + go func() { + defer close(done) + defer func() { _ = recover() }() + act.Execute() + }() + <-done + + assert.True(t, recorder.failed, "a timeout must fail the test") + assert.Contains(t, recorder.message, "not ready yet", "the last failure should be reported") + assert.Greater(t, inner.runs, 1, "should have retried before giving up") +} + +func TestEventually_RespectsTheTimeout(t *testing.T) { + act := &Eventually{Action: &fakeAction{failures: 1000}, Timeout: 200 * time.Millisecond} + recorder := &recordingT{TB: t} + act.SetState(&state.State{T: recorder}) + + start := time.Now() + done := make(chan struct{}) + go func() { + defer close(done) + defer func() { _ = recover() }() + act.Execute() + }() + <-done + elapsed := time.Since(start) + + assert.True(t, recorder.failed) + assert.Less(t, elapsed, 5*time.Second, "should give up near the timeout, not run on") +} + +func TestEventually_RealPanic_IsNotSwallowed(t *testing.T) { + // A panic from a bug in the action is not a failed attempt. Retrying it + // would hide the bug and burn the whole timeout, so it propagates. + act := &Eventually{Action: &fakeAction{panicWith: "nil map write"}, Timeout: time.Second} + act.SetState(newEventuallyState(t)) + + assert.PanicsWithValue(t, "nil map write", func() { act.Execute() }) +} + +func TestEventually_RealPanic_RestoresTheRealT(t *testing.T) { + // The recorder swallows failures so a failed attempt can be retried. If a + // panic left it in place, every later assertion in the test would be written + // to something nothing reads, turning real failures into passes. + st := newEventuallyState(t) + realT := st.T + act := &Eventually{Action: &fakeAction{panicWith: "nil map write"}, Timeout: time.Second} + act.SetState(st) + + assert.Panics(t, func() { act.Execute() }) + + assert.Same(t, realT, st.T, "the real T must be restored even when the action panics") +} + +func TestEventually_SetsStateOnTheNestedAction(t *testing.T) { + // The nested action is given the state each attempt, so it can assert + // through the recorder rather than the real T. + inner := &fakeAction{failures: 1} + act := &Eventually{Action: inner} + act.SetState(newEventuallyState(t)) + + act.Execute() + + assert.NotNil(t, inner.s, "the nested action must receive the state") +} + +func TestEventually_DefaultTimeoutIsUsedWhenUnset(t *testing.T) { + act := &Eventually{Action: &fakeAction{}} + act.SetState(newEventuallyState(t)) + + act.Execute() + + assert.Equal(t, time.Duration(0), act.Timeout, "an unset timeout stays unset and falls back to the default") +} diff --git a/tests/action/identity.go b/tests/action/identity.go index 8a5b50421b..53137ce1ad 100644 --- a/tests/action/identity.go +++ b/tests/action/identity.go @@ -55,10 +55,11 @@ func getIdentityForRequest(s *state.State, identity state.Identity, nodeIndex in // Generate/regenerate the token if: // - No token exists yet, OR - // - An audience is now available but the token was generated without one - // (this can happen when the token is created during node setup before the - // HTTP wrapper is ready, causing the audience to be unavailable at that time). - if !ok || (audience.HasValue() && !state.TokenHasAudience(token)) { + // - The token does not carry this node's current audience. It may have been + // generated during node setup before the HTTP wrapper was ready, or for an + // address the node no longer listens on: an external node binds a new port + // every start, and the node rejects a token minted for the old one. + if !ok || (audience.HasValue() && !state.TokenHasAudience(token, audience.Value())) { if s.DocumentACPType == state.SourceHubDocumentACPType || audience.HasValue() { err := fullIdent.UpdateToken( AuthTokenExpiration, diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index 606bfbf6e6..4dff913964 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -16,6 +16,7 @@ package action import ( "context" "fmt" + "strings" "sync" "github.com/multiformats/go-multiaddr" @@ -74,7 +75,7 @@ func SetupNode( ver string, ) (*state.NodeState, error) { if ver != "" { - return setupExternalNode(s, ver) + return setupExternalNode(s, cfg, ver) } if opts == nil { @@ -260,7 +261,7 @@ func discoverPeerAddresses(s *state.State, peers peerInfoProvider, isExternal bo // // If no release asset exists for this platform, the test is skipped (not // failed) and a nil error is returned. -func setupExternalNode(s *state.State, ver string) (*state.NodeState, error) { +func setupExternalNode(s *state.State, cfg NodeSetupConfig, ver string) (*state.NodeState, error) { path, skip, err := version.BinaryPath(s.Ctx, ver) if err != nil { return nil, err @@ -270,7 +271,14 @@ func setupExternalNode(s *state.State, ver string) (*state.NodeState, error) { return nil, nil } - w, err := external.NewWrapper(s.Ctx, s.T, path) + flags, unsupported := externalNodeFlags(s, cfg) + if len(unsupported) > 0 { + s.T.Skipf("external node cannot be given this test's configuration: %s", + strings.Join(unsupported, "; ")) + return nil, nil + } + + w, err := external.NewWrapper(s.Ctx, s.T, path, flags) if err != nil { return nil, err } @@ -280,6 +288,63 @@ func setupExternalNode(s *state.State, ver string) (*state.NodeState, error) { return newNodeState(s, w, w, "", true) } +// externalNodeFlags translates the configuration a native node would be given +// into command line flags for an external one, so both run the same setup. +// +// The second return holds the settings that cannot be expressed as flags. Those +// are not skippable details: the node would start with a default the test did +// not ask for and the test would still pass, so the caller skips instead. +func externalNodeFlags(s *state.State, cfg NodeSetupConfig) (flags []string, unsupported []string) { + // The node signs by default, so not signing has to be asked for. + if !cfg.EnableSigning { + flags = append(flags, "--no-signing") + } + + // Listen on the same interface a native node would. The addresses a node + // reports are asserted by some tests, so a node listening on loopback while + // its peers listen on the LAN address reports something different from them. + flags = append(flags, "--p2paddr", "/ip4/"+getIPString()+"/tcp/0") + + // The store flag takes badger or memory, so the in-memory badger the tests + // usually run is not offered. Badger on disk is what the node starts with. + switch s.DbType { + case "", BadgerIMType, BadgerFileType: + flags = append(flags, "--store", "badger") + case DefraIMType: + flags = append(flags, "--store", "memory") + default: + unsupported = append(unsupported, "store type "+string(s.DbType)) + } + + if s.DocumentACPType != "" && s.DocumentACPType != state.LocalDocumentACPType { + // source-hub needs an address and a signer the test holds in process. + unsupported = append(unsupported, "document ACP type "+string(s.DocumentACPType)) + } + + // The encryption flags only turn generation off. The tests pass their own + // keys, which there is no flag for, so a test that sets one cannot run. + if s.EnableSearchableEncryption { + unsupported = append(unsupported, "searchable encryption: the test supplies a key, and no flag sets one") + } else { + flags = append(flags, "--no-searchable-encryption") + } + if cfg.BadgerEncryption { + unsupported = append(unsupported, "badger encryption: the test supplies a key, and only --no-encryption exists") + } + + // The wrapper picks the API address itself so it knows where to reach the + // node, so a test cannot choose one. + if cfg.HTTP.HasValue() { + unsupported = append(unsupported, "custom HTTP options: the wrapper picks the address") + } + // There is no flag for the KMS. + if s.KMS != "" && s.KMS != NoneKMSType { + unsupported = append(unsupported, "KMS "+string(s.KMS)) + } + + return flags, unsupported +} + func removePeerIDFromAddr(addr []string) ([]string, error) { addrs := make([]string, len(addr)) for i, a := range addr { diff --git a/tests/action/results.go b/tests/action/results.go index 937d7e28b3..c49c407a29 100644 --- a/tests/action/results.go +++ b/tests/action/results.go @@ -157,6 +157,17 @@ func assertCollectionVersions( } } +// clientTypeForNode returns the client type used to talk to the given node. +// +// An external node runs in another process and is always reached over HTTP, so +// its results need the relaxed comparison whatever client the run selected. +func clientTypeForNode(s *state.State, nodeID int) state.ClientType { + if nodeID >= 0 && nodeID < len(s.Nodes) && s.Nodes[nodeID].IsExternal { + return state.HTTPClientType + } + return s.ClientType +} + // assertResultsEqual asserts that actual result is equal to the expected result. // // The comparison is relaxed when using client types other than goClientType. diff --git a/tests/action/utils_events.go b/tests/action/utils_events.go index 6c509c497c..1d4dd23da7 100644 --- a/tests/action/utils_events.go +++ b/tests/action/utils_events.go @@ -12,6 +12,7 @@ package action import ( + "fmt" "strconv" "time" @@ -20,6 +21,7 @@ import ( "github.com/sourcenetwork/immutable" + "github.com/sourcenetwork/defradb/client/options" "github.com/sourcenetwork/defradb/event" coreblock "github.com/sourcenetwork/defradb/internal/core/block" "github.com/sourcenetwork/defradb/tests/state" @@ -50,7 +52,16 @@ func waitForUpdateEvents( continue // node is closed } if node.IsExternal { - continue // external node: its event bus is in another process; the cross-version test polls a query to confirm sync + // A node in another process emits no events we can read, so there is + // nothing to wait for here. The write still has to reach the nodes this + // one replicates to, and normally those nodes learn what to expect from + // this event. Tell them the document ID directly instead, or they would + // wait for nothing and the test would read the data too early. + // + // A node with no replicators has no one to tell, so this does nothing + // when networking is not in use. + MarkDocsExpectedOnTargets(s, i, collectionIndex, docIDs, ident) + continue } expect := make(map[string]struct{}, len(docIDs)) @@ -125,6 +136,141 @@ func waitForUpdateEvents( } } +// MarkDocsExpectedOnTargets records that the given documents should reach every +// node the source syncs to. +// +// It is the counterpart of [updateNetworkState] for a source node whose events +// cannot be read. The head is queried from the node instead of taken from an +// event, so the nodes downstream wait on the same head either way. +func MarkDocsExpectedOnTargets( + s *state.State, + sourceNodeID int, + collectionIndex int, + docIDs map[string]struct{}, + ident immutable.Option[state.Identity], +) { + for docID := range docIDs { + // The source node wrote this document, so it must be able to report the + // commit. Skipping would record nothing to wait for, letting the + // assertions that follow pass against data that never arrived. + head, ok := latestCompositeCID(s, sourceNodeID, docID, ident) + require.True(s.T, ok, "node %d could not report the head of %s", sourceNodeID, docID) + + // Build the event, since the real one cannot be read. + evt := event.Update{ + DocID: docID, + Cid: head, + CollectionID: collectionIDForIndex(s, sourceNodeID, collectionIndex), + } + + s.Nodes[sourceNodeID].P2P.ActualDAGHeads[docID] = state.DocHeadState{CID: head} + + for targetID := range s.Nodes[sourceNodeID].P2P.Replicators { + s.Nodes[targetID].P2P.ExpectedDAGHeads[docID] = append( + s.Nodes[targetID].P2P.ExpectedDAGHeads[docID], + state.ExpectedHead{CID: head, SourceNodeID: sourceNodeID}, + ) + } + + // Subscribers are reached over connections rather than replicators, so + // they need the same walk the native path does. + updateConnectedNodes( + s, sourceNodeID, sourceNodeID, map[int]struct{}{}, ident, + collectionIndex, docIndexForID(s, collectionIndex, docID), evt, + ) + } +} + +// collectionIDForIndex returns the collection ID for a collection index on a node. +func collectionIDForIndex(s *state.State, nodeID int, collectionIndex int) string { + collections := s.Nodes[nodeID].Collections + if collectionIndex < 0 || collectionIndex >= len(collections) { + return "" + } + return collections[collectionIndex].Version().CollectionID +} + +// docIndexForID returns the index a document was added under, or -1 if unknown. +func docIndexForID(s *state.State, collectionIndex int, docID string) int { + s.DocIDsLock.RLock() + defer s.DocIDsLock.RUnlock() + + if collectionIndex < 0 || collectionIndex >= len(s.DocIDs) { + return -1 + } + for i, id := range s.DocIDs[collectionIndex] { + if id.String() == docID { + return i + } + } + return -1 +} + +// latestCompositeCID asks the node for the newest composite commit of a +// document. +// +// A merge event reports the composite commit, so this is the same CID the native +// path takes from that event. +// +// ident is the identity the document was written with. A document protected by +// document ACP is invisible to an unidentified reader, so without it the query +// returns nothing and the head cannot be found. +func latestCompositeCID( + s *state.State, + nodeID int, + docID string, + ident immutable.Option[state.Identity], +) (cid.Cid, bool) { + reqOption := options.ExecRequest() + identOption := getIdentityForRequestSpecificToNode(s, ident, nodeID) + if identOption.HasValue() { + reqOption.SetIdentity(identOption.Value()) + } + + result := s.Nodes[nodeID].ExecRequest( + s.Ctx, + fmt.Sprintf( + `query { _commits(docID: %q, filter: {fieldName: {_eq: "_C"}}, order: {height: DESC}, limit: 1) { cid } }`, + docID, + ), + reqOption, + ) + if len(result.GQL.Errors) > 0 { + return cid.Cid{}, false + } + + data, ok := result.GQL.Data.(map[string]any) + if !ok { + return cid.Cid{}, false + } + + var cidStr string + switch commits := data["_commits"].(type) { + case []any: + if len(commits) == 0 { + return cid.Cid{}, false + } + commit, ok := commits[0].(map[string]any) + if !ok { + return cid.Cid{}, false + } + cidStr, _ = commit["cid"].(string) + case []map[string]any: + if len(commits) == 0 { + return cid.Cid{}, false + } + cidStr, _ = commits[0]["cid"].(string) + default: + return cid.Cid{}, false + } + + parsed, err := cid.Decode(cidStr) + if err != nil { + return cid.Cid{}, false + } + return parsed, true +} + // updateNetworkState updates the network state by checking which // nodes should receive the updated document in the given update event. func updateNetworkState(s *state.State, nodeID int, evt event.Update, ident immutable.Option[state.Identity]) { diff --git a/tests/action/wait_for_peer_events.go b/tests/action/wait_for_peer_events.go index 05487efe10..4a827c7b63 100644 --- a/tests/action/wait_for_peer_events.go +++ b/tests/action/wait_for_peer_events.go @@ -63,6 +63,14 @@ func (a *WaitForPeersEvents) Execute() { } sourceNode := a.s.Nodes[a.NodeID] + + // A node in another process emits its peer events on a bus we cannot read, + // so there is nothing to wait for. The connection itself is still made, and + // the actions that follow verify it by talking to the node. + if sourceNode.IsExternal { + return + } + expectedPeers := make(map[string]map[string]bool) addExpectedPeers := func(topic string, peerNodeIDs []int) { diff --git a/tests/clients/external/wrapper.go b/tests/clients/external/wrapper.go index 2480248c63..4fbd7d8f7e 100644 --- a/tests/clients/external/wrapper.go +++ b/tests/clients/external/wrapper.go @@ -78,7 +78,7 @@ type Wrapper struct { // A temporary rootdir and a free API port are chosen internally. The P2P // listener uses port 0 so the node picks a free port at bind time; read its // real address back with PeerInfo. Use Host for the API URL. -func NewWrapper(ctx context.Context, t testing.TB, binaryPath string) (*Wrapper, error) { +func NewWrapper(ctx context.Context, t testing.TB, binaryPath string, extraFlags []string) (*Wrapper, error) { // The API port is chosen before start, so another process can grab it in the // gap before the child binds. Retry a few times on a start/health failure. var lastErr error @@ -91,7 +91,7 @@ func NewWrapper(ctx context.Context, t testing.TB, binaryPath string) (*Wrapper, } return nil, err } - w, err := startWrapper(ctx, t, binaryPath) + w, err := startWrapper(ctx, t, binaryPath, extraFlags) if err == nil { return w, nil } @@ -101,7 +101,7 @@ func NewWrapper(ctx context.Context, t testing.TB, binaryPath string) (*Wrapper, } // startWrapper makes one attempt to start and reach a node. -func startWrapper(ctx context.Context, t testing.TB, binaryPath string) (*Wrapper, error) { +func startWrapper(ctx context.Context, t testing.TB, binaryPath string, extraFlags []string) (*Wrapper, error) { apiPort, err := freePort() if err != nil { return nil, errors.Wrap("failed to find free api port", err) @@ -113,14 +113,18 @@ func startWrapper(ctx context.Context, t testing.TB, binaryPath string) (*Wrappe apiURL := fmt.Sprintf("127.0.0.1:%d", apiPort) - cmd := exec.CommandContext(ctx, binaryPath, "start", + // The address and rootdir are chosen here because the wrapper needs to know + // them. Everything else about the node comes from the caller, so it can + // match the configuration a native node would be given. + args := []string{"start", "--url", apiURL, - "--p2paddr", "/ip4/127.0.0.1/tcp/0", - "--store", "badger", "--development", "--no-keyring", "--rootdir", rootDir, - ) + } + args = append(args, extraFlags...) + + cmd := exec.CommandContext(ctx, binaryPath, args...) stderr := newRingBuffer(64 * 1024) stdoutPipe, err := cmd.StdoutPipe() diff --git a/tests/clients/external/wrapper_stub_test.go b/tests/clients/external/wrapper_stub_test.go index bb113b0243..76453a103a 100644 --- a/tests/clients/external/wrapper_stub_test.go +++ b/tests/clients/external/wrapper_stub_test.go @@ -63,7 +63,7 @@ func TestNewWrapper_CtxCancelled_ReturnsPromptly(t *testing.T) { t.Setenv("STUB_MODE", "unhealthy") start := time.Now() - w, err := NewWrapper(ctx, t, binaryPath) + w, err := NewWrapper(ctx, t, binaryPath, nil) elapsed := time.Since(start) require.Error(t, err) @@ -80,7 +80,7 @@ func TestNewWrapper_StartFailure_ReturnsError(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - w, err := NewWrapper(ctx, t, missingPath) + w, err := NewWrapper(ctx, t, missingPath, nil) require.Error(t, err) assert.Nil(t, w) @@ -94,7 +94,7 @@ func TestWrapper_Close_Idempotent(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - w, err := NewWrapper(ctx, t, binaryPath) + w, err := NewWrapper(ctx, t, binaryPath, nil) require.NoError(t, err) require.NotNil(t, w) diff --git a/tests/clients/external/wrapper_test.go b/tests/clients/external/wrapper_test.go index b2133141de..4cca6ce788 100644 --- a/tests/clients/external/wrapper_test.go +++ b/tests/clients/external/wrapper_test.go @@ -44,7 +44,7 @@ func TestExternalWrapper(t *testing.T) { t.Skip("no v1.0.0 asset for this platform") } - w, err := NewWrapper(ctx, t, path) + w, err := NewWrapper(ctx, t, path, nil) require.NoError(t, err) defer w.Close() diff --git a/tests/integration/acp/dac/branchable/peer_test.go b/tests/integration/acp/dac/branchable/peer_test.go index 34eb104e7d..3731608c0d 100644 --- a/tests/integration/acp/dac/branchable/peer_test.go +++ b/tests/integration/acp/dac/branchable/peer_test.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) @@ -35,6 +36,14 @@ const commitsQuery = ` // at the sync layer and never reach node 1 - the owner sees nothing on the peer. func TestACP_P2PBranchableCollectionNotSyncedWithoutNodeCollectionAccess_LocalACP(t *testing.T) { test := testUtils.TestCase{ + // The document is written without an identity into an ACP gated collection, so + // the head read-back that works out what the peer should receive is + // unauthenticated and finds nothing. + // https://github.com/sourcenetwork/defradb/issues/5196 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, @@ -95,6 +104,14 @@ func TestACP_P2PBranchableCollectionSyncedWithNodeCollectionAccess_LocalACP(t *t ownerCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ + // The document is written without an identity into an ACP gated collection, so + // the head read-back that works out what the peer should receive is + // unauthenticated and finds nothing. + // https://github.com/sourcenetwork/defradb/issues/5196 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, @@ -189,6 +206,14 @@ func TestACP_P2PBranchableCollectionSharedReaderCanReadOnPeer_LocalACP(t *testin afterCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ + // The document is written without an identity into an ACP gated collection, so + // the head read-back that works out what the peer should receive is + // unauthenticated and finds nothing. + // https://github.com/sourcenetwork/defradb/issues/5196 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, diff --git a/tests/integration/acp/dac/p2p/sync_shared_field_test.go b/tests/integration/acp/dac/p2p/sync_shared_field_test.go index ad08d611bf..5afaf9bd8f 100644 --- a/tests/integration/acp/dac/p2p/sync_shared_field_test.go +++ b/tests/integration/acp/dac/p2p/sync_shared_field_test.go @@ -78,7 +78,18 @@ func sharedFieldSyncTestCase(grantedAge, otherAge int) testUtils.TestCase { ), // Signing/encryption change block cids per document, which would break the // shared-block premise this test relies on. - MultiplierExcludes: []string{multiplier.SignedDocs, multiplier.EncryptedDocs}, + // + // This test withholds one document from node 1 on purpose, so one of the + // heads never arrives. An external node is polled for its commits instead + // of read from its event bus, and that poll cannot express a head that is + // not coming. + // https://github.com/sourcenetwork/defradb/issues/5193 + MultiplierExcludes: []string{ + multiplier.SignedDocs, + multiplier.EncryptedDocs, + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/add_p2p_collection_test.go b/tests/integration/acp/nac/add_p2p_collection_test.go index d638335825..b103b8ce27 100644 --- a/tests/integration/acp/nac/add_p2p_collection_test.go +++ b/tests/integration/acp/nac/add_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesAddP2PCollection_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -72,6 +80,13 @@ func TestNAC_GatesAddP2PCollection_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesAddP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -121,6 +136,13 @@ func TestNAC_GatesAddP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesAddP2PCollection_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/add_p2p_document_test.go b/tests/integration/acp/nac/add_p2p_document_test.go index d764abc32c..67c971639f 100644 --- a/tests/integration/acp/nac/add_p2p_document_test.go +++ b/tests/integration/acp/nac/add_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesAddP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -80,6 +88,13 @@ func TestNAC_GatesAddP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) { func TestNAC_GatesAddP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -137,6 +152,13 @@ func TestNAC_GatesAddP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesAddP2PDocument_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/add_p2p_replicator_test.go b/tests/integration/acp/nac/add_p2p_replicator_test.go index 75f6d2ecc9..49041f0ed2 100644 --- a/tests/integration/acp/nac/add_p2p_replicator_test.go +++ b/tests/integration/acp/nac/add_p2p_replicator_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesAddP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -56,6 +64,13 @@ func TestNAC_GatesAddP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesAddP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -82,6 +97,13 @@ func TestNAC_GatesAddP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesAddP2PReplicator_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/connect_p2p_peer_test.go b/tests/integration/acp/nac/connect_p2p_peer_test.go index 9bb3a0b332..6df5f132ad 100644 --- a/tests/integration/acp/nac/connect_p2p_peer_test.go +++ b/tests/integration/acp/nac/connect_p2p_peer_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesConnectP2PPeer_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -56,6 +64,13 @@ func TestNAC_GatesConnectP2PPeer_AuthorizedIdentity_AllowAccess(t *testing.T) { func TestNAC_GatesConnectP2PPeer_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -82,6 +97,13 @@ func TestNAC_GatesConnectP2PPeer_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesConnectP2PPeer_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/delete_p2p_collection_test.go b/tests/integration/acp/nac/delete_p2p_collection_test.go index 7262f05d97..a136055c4b 100644 --- a/tests/integration/acp/nac/delete_p2p_collection_test.go +++ b/tests/integration/acp/nac/delete_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesDeleteP2PCollection_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -77,6 +85,13 @@ func TestNAC_GatesDeleteP2PCollection_AuthorizedIdentity_AllowAccess(t *testing. func TestNAC_GatesDeleteP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -131,6 +146,13 @@ func TestNAC_GatesDeleteP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T func TestNAC_GatesDeleteP2PCollection_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/delete_p2p_document_test.go b/tests/integration/acp/nac/delete_p2p_document_test.go index 68b0d223a9..7733e8c7c4 100644 --- a/tests/integration/acp/nac/delete_p2p_document_test.go +++ b/tests/integration/acp/nac/delete_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesDeleteP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -87,6 +95,13 @@ func TestNAC_GatesDeleteP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesDeleteP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -151,6 +166,13 @@ func TestNAC_GatesDeleteP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) func TestNAC_GatesDeleteP2PDocument_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/delete_p2p_replicator_test.go b/tests/integration/acp/nac/delete_p2p_replicator_test.go index 7ed4f009fc..4f7f60aa4e 100644 --- a/tests/integration/acp/nac/delete_p2p_replicator_test.go +++ b/tests/integration/acp/nac/delete_p2p_replicator_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesDeleteP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -61,6 +69,13 @@ func TestNAC_GatesDeleteP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing. func TestNAC_GatesDeleteP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -100,6 +115,13 @@ func TestNAC_GatesDeleteP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T func TestNAC_GatesDeleteP2PReplicator_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/disconnect_p2p_peer_test.go b/tests/integration/acp/nac/disconnect_p2p_peer_test.go index 1432ab7526..d888d8a9fe 100644 --- a/tests/integration/acp/nac/disconnect_p2p_peer_test.go +++ b/tests/integration/acp/nac/disconnect_p2p_peer_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesDisconnectP2PPeer_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -72,6 +80,13 @@ func TestNAC_GatesDisconnectP2PPeer_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesDisconnectP2PPeer_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), @@ -95,6 +110,13 @@ func TestNAC_GatesDisconnectP2PPeer_NoIdentity_NotAuthorizedError(t *testing.T) func TestNAC_GatesDisconnectP2PPeer_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/get_p2p_active_peers_test.go b/tests/integration/acp/nac/get_p2p_active_peers_test.go index 09d150bcf7..334697c46f 100644 --- a/tests/integration/acp/nac/get_p2p_active_peers_test.go +++ b/tests/integration/acp/nac/get_p2p_active_peers_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesGetActivePeers_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -56,6 +64,13 @@ func TestNAC_GatesGetActivePeers_AuthorizedIdentity_AllowAccess(t *testing.T) { func TestNAC_GatesGetActivePeers_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -81,6 +96,13 @@ func TestNAC_GatesGetActivePeers_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesGetActivePeers_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/get_p2p_peer_info_test.go b/tests/integration/acp/nac/get_p2p_peer_info_test.go index 1ac33850e3..20ecee4731 100644 --- a/tests/integration/acp/nac/get_p2p_peer_info_test.go +++ b/tests/integration/acp/nac/get_p2p_peer_info_test.go @@ -16,12 +16,20 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" "github.com/sourcenetwork/immutable" ) func TestNAC_GatesGetP2PPeerInfo_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -55,6 +63,13 @@ func TestNAC_GatesGetP2PPeerInfo_AuthorizedIdentity_AllowAccess(t *testing.T) { func TestNAC_GatesGetP2PPeerInfo_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -80,6 +95,13 @@ func TestNAC_GatesGetP2PPeerInfo_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesGetP2PPeerInfo_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beggining is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/list_p2p_collection_test.go b/tests/integration/acp/nac/list_p2p_collection_test.go index 616cf05ca6..0026fa6ace 100644 --- a/tests/integration/acp/nac/list_p2p_collection_test.go +++ b/tests/integration/acp/nac/list_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesListP2PCollection_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -77,6 +85,13 @@ func TestNAC_GatesListP2PCollection_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesListP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -130,6 +145,13 @@ func TestNAC_GatesListP2PCollection_NoIdentity_NotAuthorizedError(t *testing.T) func TestNAC_GatesListP2PCollection_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/list_p2p_document_test.go b/tests/integration/acp/nac/list_p2p_document_test.go index c1da3952ac..5920fcb932 100644 --- a/tests/integration/acp/nac/list_p2p_document_test.go +++ b/tests/integration/acp/nac/list_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesListP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -87,6 +95,13 @@ func TestNAC_GatesListP2PDocument_AuthorizedIdentity_AllowAccess(t *testing.T) { func TestNAC_GatesListP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -148,6 +163,13 @@ func TestNAC_GatesListP2PDocument_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesListP2PDocument_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/list_p2p_replicator_test.go b/tests/integration/acp/nac/list_p2p_replicator_test.go index 18dbe4f054..c59294d25c 100644 --- a/tests/integration/acp/nac/list_p2p_replicator_test.go +++ b/tests/integration/acp/nac/list_p2p_replicator_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesListP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -62,6 +70,13 @@ func TestNAC_GatesListP2PReplicator_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesListP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -100,6 +115,13 @@ func TestNAC_GatesListP2PReplicator_NoIdentity_NotAuthorizedError(t *testing.T) func TestNAC_GatesListP2PReplicator_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/add_p2p_collection_test.go b/tests/integration/acp/nac/relation_admin/add_p2p_collection_test.go index d5a9166cd3..38f1b294b6 100644 --- a/tests/integration/acp/nac/relation_admin/add_p2p_collection_test.go +++ b/tests/integration/acp/nac/relation_admin/add_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanAddP2PCollection(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/add_p2p_document_test.go b/tests/integration/acp/nac/relation_admin/add_p2p_document_test.go index 2a2e06ffe2..5d48c48789 100644 --- a/tests/integration/acp/nac/relation_admin/add_p2p_document_test.go +++ b/tests/integration/acp/nac/relation_admin/add_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanAddP2PDocument(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/add_p2p_replicator_test.go b/tests/integration/acp/nac/relation_admin/add_p2p_replicator_test.go index eec0d6870d..766b9c54c9 100644 --- a/tests/integration/acp/nac/relation_admin/add_p2p_replicator_test.go +++ b/tests/integration/acp/nac/relation_admin/add_p2p_replicator_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanAddP2PReplicator(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/connect_p2p_peer_test.go b/tests/integration/acp/nac/relation_admin/connect_p2p_peer_test.go index 74911daa01..86fe5a1dae 100644 --- a/tests/integration/acp/nac/relation_admin/connect_p2p_peer_test.go +++ b/tests/integration/acp/nac/relation_admin/connect_p2p_peer_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanConnectP2PPeer(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/delete_p2p_collection_test.go b/tests/integration/acp/nac/relation_admin/delete_p2p_collection_test.go index 450202ad5e..0f89454844 100644 --- a/tests/integration/acp/nac/relation_admin/delete_p2p_collection_test.go +++ b/tests/integration/acp/nac/relation_admin/delete_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanDeleteP2PCollection(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/delete_p2p_document_test.go b/tests/integration/acp/nac/relation_admin/delete_p2p_document_test.go index e90e269d36..c233811586 100644 --- a/tests/integration/acp/nac/relation_admin/delete_p2p_document_test.go +++ b/tests/integration/acp/nac/relation_admin/delete_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanDeleteP2PDocument(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/delete_p2p_replicator_test.go b/tests/integration/acp/nac/relation_admin/delete_p2p_replicator_test.go index 9ad0b71829..ac68e258b5 100644 --- a/tests/integration/acp/nac/relation_admin/delete_p2p_replicator_test.go +++ b/tests/integration/acp/nac/relation_admin/delete_p2p_replicator_test.go @@ -18,11 +18,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanDeleteP2PReplicator(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/get_p2p_active_peers_test.go b/tests/integration/acp/nac/relation_admin/get_p2p_active_peers_test.go index ef6eca193e..ec31640c86 100644 --- a/tests/integration/acp/nac/relation_admin/get_p2p_active_peers_test.go +++ b/tests/integration/acp/nac/relation_admin/get_p2p_active_peers_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanGetActivePeers(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/get_p2p_peer_info_test.go b/tests/integration/acp/nac/relation_admin/get_p2p_peer_info_test.go index 0a7a698396..12a6d11721 100644 --- a/tests/integration/acp/nac/relation_admin/get_p2p_peer_info_test.go +++ b/tests/integration/acp/nac/relation_admin/get_p2p_peer_info_test.go @@ -18,11 +18,19 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanGetP2PPeerInfo(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/list_p2p_collection_test.go b/tests/integration/acp/nac/relation_admin/list_p2p_collection_test.go index 1335291ccc..90fe0ef8df 100644 --- a/tests/integration/acp/nac/relation_admin/list_p2p_collection_test.go +++ b/tests/integration/acp/nac/relation_admin/list_p2p_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanListP2PCollection(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/list_p2p_document_test.go b/tests/integration/acp/nac/relation_admin/list_p2p_document_test.go index 472c4a4175..6d1821b01d 100644 --- a/tests/integration/acp/nac/relation_admin/list_p2p_document_test.go +++ b/tests/integration/acp/nac/relation_admin/list_p2p_document_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanListP2PDocument(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/list_p2p_replicator_test.go b/tests/integration/acp/nac/relation_admin/list_p2p_replicator_test.go index 15096076a9..9cac6e08de 100644 --- a/tests/integration/acp/nac/relation_admin/list_p2p_replicator_test.go +++ b/tests/integration/acp/nac/relation_admin/list_p2p_replicator_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanListP2PReplicator(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/sync_p2p_branchable_collection_test.go b/tests/integration/acp/nac/relation_admin/sync_p2p_branchable_collection_test.go index 99e8d6438d..b546f3ead4 100644 --- a/tests/integration/acp/nac/relation_admin/sync_p2p_branchable_collection_test.go +++ b/tests/integration/acp/nac/relation_admin/sync_p2p_branchable_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanSyncP2PBranchableCollection(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/sync_p2p_collection_versions_test.go b/tests/integration/acp/nac/relation_admin/sync_p2p_collection_versions_test.go index ca78fe3ccf..bb649764f5 100644 --- a/tests/integration/acp/nac/relation_admin/sync_p2p_collection_versions_test.go +++ b/tests/integration/acp/nac/relation_admin/sync_p2p_collection_versions_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanSyncP2PCollectionVersions(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/relation_admin/sync_p2p_documents_test.go b/tests/integration/acp/nac/relation_admin/sync_p2p_documents_test.go index 356bfd7114..717a1b9f02 100644 --- a/tests/integration/acp/nac/relation_admin/sync_p2p_documents_test.go +++ b/tests/integration/acp/nac/relation_admin/sync_p2p_documents_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_AdminRelation_CanSyncP2PDocuments(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, diff --git a/tests/integration/acp/nac/sync_p2p_branchable_collection_test.go b/tests/integration/acp/nac/sync_p2p_branchable_collection_test.go index 06ab87733c..409823174b 100644 --- a/tests/integration/acp/nac/sync_p2p_branchable_collection_test.go +++ b/tests/integration/acp/nac/sync_p2p_branchable_collection_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesSyncP2PBranchableCollection_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -79,6 +87,13 @@ func TestNAC_GatesSyncP2PBranchableCollection_AuthorizedIdentity_AllowAccess(t * func TestNAC_GatesSyncP2PBranchableCollection_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -121,6 +136,13 @@ func TestNAC_GatesSyncP2PBranchableCollection_NoIdentity_NotAuthorizedError(t *t func TestNAC_GatesSyncP2PBranchableCollection_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/sync_p2p_collection_versions_test.go b/tests/integration/acp/nac/sync_p2p_collection_versions_test.go index 9093660430..a26fba1c06 100644 --- a/tests/integration/acp/nac/sync_p2p_collection_versions_test.go +++ b/tests/integration/acp/nac/sync_p2p_collection_versions_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesSyncP2PCollectionVersions_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -68,6 +76,13 @@ func TestNAC_GatesSyncP2PCollectionVersions_AuthorizedIdentity_AllowAccess(t *te func TestNAC_GatesSyncP2PCollectionVersions_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -94,6 +109,13 @@ func TestNAC_GatesSyncP2PCollectionVersions_NoIdentity_NotAuthorizedError(t *tes func TestNAC_GatesSyncP2PCollectionVersions_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/acp/nac/sync_p2p_documents_test.go b/tests/integration/acp/nac/sync_p2p_documents_test.go index b674a2541f..c9a1988057 100644 --- a/tests/integration/acp/nac/sync_p2p_documents_test.go +++ b/tests/integration/acp/nac/sync_p2p_documents_test.go @@ -19,11 +19,19 @@ import ( acpTypes "github.com/sourcenetwork/defradb/acp/types" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestNAC_GatesSyncP2PDocuments_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedClientTypes: immutable.Some( []state.ClientType{ state.HTTPClientType, @@ -83,6 +91,13 @@ func TestNAC_GatesSyncP2PDocuments_AuthorizedIdentity_AllowAccess(t *testing.T) func TestNAC_GatesSyncP2PDocuments_NoIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), @@ -126,6 +141,13 @@ func TestNAC_GatesSyncP2PDocuments_NoIdentity_NotAuthorizedError(t *testing.T) { func TestNAC_GatesSyncP2PDocuments_WrongIdentity_NotAuthorizedError(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ // Doing this in the beginning is important to start all nodes with NAC enabled. testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/apply_multipliers_test.go b/tests/integration/apply_multipliers_test.go index 8ea8085c53..42d0b568db 100644 --- a/tests/integration/apply_multipliers_test.go +++ b/tests/integration/apply_multipliers_test.go @@ -16,7 +16,10 @@ import ( "github.com/stretchr/testify/assert" + "github.com/sourcenetwork/immutable" + defraMultiplier "github.com/sourcenetwork/defradb/tests/multiplier" + "github.com/sourcenetwork/defradb/tests/state" ) func TestApplyTestCaseLevelMultipliers_WithSignedDocs_EnablesSigning(t *testing.T) { @@ -85,3 +88,62 @@ func TestApplyTestCaseLevelMultipliers_NeverDowngradesEnableSigning(t *testing.T applyTestCaseLevelMultipliers(tc, "secondary-index") assert.True(t, tc.EnableSigning, "unrelated multipliers must not touch the flag") } + +func TestExternalNodeMultiplierUnsupported(t *testing.T) { + goOnly := immutable.Some([]state.ClientType{state.GoClientType}) + + tests := []struct { + name string + clientTypes immutable.Option[[]state.ClientType] + activeNames string + wantName string + wantSkip bool + }{ + { + name: "no supported client types runs", + activeNames: defraMultiplier.CrossVersionOldSource, + }, + { + name: "http client supported runs", + clientTypes: immutable.Some( + []state.ClientType{state.GoClientType, state.HTTPClientType}, + ), + activeNames: defraMultiplier.CrossVersionOldSource, + }, + { + name: "go client only skips", + clientTypes: goOnly, + activeNames: defraMultiplier.CrossVersionNewSource, + wantName: defraMultiplier.CrossVersionNewSource, + wantSkip: true, + }, + { + name: "in process multiplier runs", + clientTypes: goOnly, + activeNames: defraMultiplier.SignedDocs, + }, + { + // The name is reported so the skip message can say which multiplier it was. + name: "cross version among others skips", + clientTypes: goOnly, + activeNames: defraMultiplier.SignedDocs + ", " + defraMultiplier.CrossVersionOldSource, + wantName: defraMultiplier.CrossVersionOldSource, + wantSkip: true, + }, + { + name: "no active multipliers runs", + clientTypes: goOnly, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tc := &TestCase{SupportedClientTypes: test.clientTypes} + + name, skip := externalNodeMultiplierUnsupported(tc, test.activeNames) + + assert.Equal(t, test.wantSkip, skip) + assert.Equal(t, test.wantName, name) + }) + } +} diff --git a/tests/integration/encryption/peer_nac_test.go b/tests/integration/encryption/peer_nac_test.go index eb4c747483..34baa3f080 100644 --- a/tests/integration/encryption/peer_nac_test.go +++ b/tests/integration/encryption/peer_nac_test.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) @@ -25,6 +26,13 @@ import ( // authorized identity. Sync must succeed end-to-end. func TestDocEncryptionNAC_SyncBranchableCollection_AuthorizedIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ @@ -76,6 +84,13 @@ func TestDocEncryptionNAC_SyncBranchableCollection_AuthorizedIdentity_AllowAcces // unauthorized identity. Sync must be denied. func TestDocEncryptionNAC_SyncBranchableCollection_UnauthorizedIdentity_DenyAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ @@ -127,6 +142,13 @@ func TestDocEncryptionNAC_SyncBranchableCollection_UnauthorizedIdentity_DenyAcce // identity. Sync must be denied. func TestDocEncryptionNAC_SyncBranchableCollection_NoIdentity_DenyAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ @@ -179,6 +201,13 @@ func TestDocEncryptionNAC_SyncBranchableCollection_NoIdentity_DenyAccess(t *test // succeeds end-to-end. func TestDocEncryptionNAC_SyncBranchableCollection_GrantedRelation_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ @@ -247,6 +276,13 @@ func TestDocEncryptionNAC_SyncBranchableCollection_GrantedRelation_AllowAccess(t // revoked. The subsequent sync must be denied. func TestDocEncryptionNAC_SyncBranchableCollection_RevokedRelation_DenyAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ @@ -319,6 +355,13 @@ func TestDocEncryptionNAC_SyncBranchableCollection_RevokedRelation_DenyAccess(t // publishing peer. func TestDocEncryptionNAC_GossipSync_AuthorizedNodeIdentity_AllowAccess(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, KMS: testUtils.KMS{Activated: true}, SupportedClientTypes: immutable.Some( []state.ClientType{ diff --git a/tests/integration/events.go b/tests/integration/events.go index a1fc654d28..a8f4353258 100644 --- a/tests/integration/events.go +++ b/tests/integration/events.go @@ -15,6 +15,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "time" "github.com/ipfs/go-cid" @@ -25,6 +26,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/event" coreblock "github.com/sourcenetwork/defradb/internal/core/block" + "github.com/sourcenetwork/defradb/tests/action" "github.com/sourcenetwork/defradb/tests/state" ) @@ -162,7 +164,13 @@ func waitForUpdateEvents( continue // node is closed } if node.IsExternal { - continue // external node: its event bus is in another process; the cross-version test polls a query to confirm sync + // A node in another process emits no events we can read, so there is + // nothing to wait for here. The write still has to reach the nodes this + // one syncs to, and normally those nodes learn what to expect from this + // event. Read the head back with a query and record it for them, or + // they would wait for nothing and the test would read stale data. + action.MarkDocsExpectedOnTargets(s, i, collectionIndex, docIDs, ident) + continue } expect := make(map[string]struct{}, len(docIDs)) @@ -258,9 +266,6 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { if node.Closed { continue // node is closed } - if node.IsExternal { - continue // external node: its event bus is in another process; the cross-version test polls a query to confirm sync - } // Build pending set keeping only the latest CID per (key, source) pair. // Heads are appended in order, so the last head from each source is the latest. @@ -299,6 +304,15 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { totalPending += len(cidSet) } + // A node in another process has no event bus we can read, so ask it for + // the commits instead. Waiting for the specific head matters: the + // document itself already exists after the first write, so waiting only + // for that would return immediately on every later update. + if node.IsExternal { + waitForHeadsOnNode(s, node, pending) + continue + } + for totalPending > 0 { var evt event.MergeComplete select { @@ -338,6 +352,162 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { } } +// waitForHeadsOnNode waits until every expected head has arrived on the node. +// +// It asks the node for its commits instead of reading its event bus, so it works +// for a node in another process. It waits on the head rather than the document +// because the document exists from the first write, while each update adds a +// new head. +func waitForHeadsOnNode(s *state.State, node *state.NodeState, pending map[string]map[cid.Cid]struct{}) { + type wanted struct { + key string + cid cid.Cid + } + var heads []wanted + for key, cidSet := range pending { + // A collection level key has no document to ask about, and only a + // document ID parses as one. + if _, err := client.NewDocIDFromString(key); err != nil { + continue + } + for c := range cidSet { + heads = append(heads, wanted{key: key, cid: c}) + } + } + if len(heads) == 0 { + return + } + + // A head that is readable elsewhere should become readable here too, so the + // wait covers the merge and not just the block arriving. A head that deleted + // the document is readable nowhere, so waiting for it would never finish. + // + // Read up front, before the target catches up, so it reflects the writer. + wantDoc := make(map[string]bool, len(heads)) + for _, head := range heads { + if _, seen := wantDoc[head.key]; seen { + continue + } + wantDoc[head.key] = anyNodeHasDocExcept(s, node, head.key) + } + + deadline := time.Now().Add(30 * eventTimeout) + for { + missing := make([]string, 0, len(heads)) + for _, head := range heads { + if !hasCommit(s, node, head.key, head.cid) { + missing = append(missing, head.cid.String()) + continue + } + // The block is stored before it is merged, so the head arriving does + // not mean the document can be read yet. Wait for that too, unless + // the head deleted the document. + if wantDoc[head.key] && !hasDoc(s, node, head.key) { + missing = append(missing, head.cid.String()) + continue + } + node.P2P.ActualDAGHeads[head.key] = state.DocHeadState{CID: head.cid} + } + if len(missing) == 0 { + return + } + if time.Now().After(deadline) { + require.Fail(s.T, "timeout waiting for commits to sync to external node", + "still missing: %v", missing) + } + time.Sleep(100 * time.Millisecond) + } +} + +// errCollectionVersionNotFound is returned when a node is asked about a commit +// whose collection version it does not have. +const errCollectionVersionNotFound = "failed to get collection by version ID" + +// anyNodeHasDocExcept reports whether any other node still holds the document, +// which stands in for "the writer did not delete it". +func anyNodeHasDocExcept(s *state.State, except *state.NodeState, docID string) bool { + for _, node := range s.Nodes { + if node == except || node.Closed { + continue + } + if hasDoc(s, node, docID) { + return true + } + } + return false +} + +// hasDoc reports whether the node can read the given document in any of its +// collections. +// +// Weaker than checking the head, so it is only used when the node cannot answer +// about commits at all. +func hasDoc(s *state.State, node *state.NodeState, docID string) bool { + for _, col := range node.Collections { + result := node.ExecRequest( + s.Ctx, + fmt.Sprintf(`query { %s(docID: %q) { _docID } }`, col.Name(), docID), + ) + if len(result.GQL.Errors) > 0 { + continue + } + + data, ok := result.GQL.Data.(map[string]any) + if !ok { + continue + } + + switch docs := data[col.Name()].(type) { + case []any: + if len(docs) > 0 { + return true + } + case []map[string]any: + if len(docs) > 0 { + return true + } + } + } + return false +} + +// hasCommit reports whether the node holds the given commit of a document. +func hasCommit(s *state.State, node *state.NodeState, docID string, target cid.Cid) bool { + result := node.ExecRequest( + s.Ctx, + fmt.Sprintf(`query { _commits(docID: %q, cid: %q) { cid } }`, docID, target.String()), + ) + for _, err := range result.GQL.Errors { + // The node holds the block but cannot describe it, so there is nothing + // left to ask. Treat it as arrived, since waiting longer never resolves. + // + // Weaker than matching the head: a test that updates the document this + // way can read the value from before the update. + if strings.Contains(err.Error(), errCollectionVersionNotFound) { + return true + } + } + // Asking for a commit the node does not hold is an error rather than an + // empty result, so treat any other error as not yet arrived. + if len(result.GQL.Errors) > 0 { + return false + } + + data, ok := result.GQL.Data.(map[string]any) + if !ok { + return false + } + + switch commits := data["_commits"].(type) { + case []any: + return len(commits) > 0 + case []map[string]any: + return len(commits) > 0 + default: + return false + } +} + func waitForSESync(s *state.State, action WaitForSESync) { var docIDsToWait []string s.DocIDsLock.RLock() diff --git a/tests/integration/identity.go b/tests/integration/identity.go index 7f70edf24e..4bd141a6c5 100644 --- a/tests/integration/identity.go +++ b/tests/integration/identity.go @@ -86,10 +86,11 @@ func getIdentityForRequest(s *state.State, identity state.Identity, nodeIndex in // Generate/regenerate the token if: // - No token exists yet, OR - // - An audience is now available but the token was generated without one - // (this can happen when the token is created during node setup before the - // HTTP wrapper is ready, causing the audience to be unavailable at that time). - if !ok || (audience.HasValue() && !state.TokenHasAudience(token)) { + // - The token does not carry this node's current audience. It may have been + // generated during node setup before the HTTP wrapper was ready, or for an + // address the node no longer listens on: an external node binds a new port + // every start, and the node rejects a token minted for the old one. + if !ok || (audience.HasValue() && !state.TokenHasAudience(token, audience.Value())) { if s.DocumentACPType == state.SourceHubDocumentACPType || audience.HasValue() { err := fullIdent.UpdateToken( action.AuthTokenExpiration, diff --git a/tests/integration/index/vector_p2p_test.go b/tests/integration/index/vector_p2p_test.go index 2463d7d64a..e6b917485e 100644 --- a/tests/integration/index/vector_p2p_test.go +++ b/tests/integration/index/vector_p2p_test.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" ) // A document written on one peer and synced to another is added to the replica's graph, so a @@ -25,6 +26,12 @@ import ( // not just direct writes. The vector @index directive is in the schema so both peers build it the same way. func TestVectorIndexP2P_ReplicatedDoc_IsSearchableOnReplica(t *testing.T) { test := testUtils.TestCase{ + // The vectorIndex directive does not exist in the older release. + // https://github.com/sourcenetwork/defradb/issues/5121 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/net/simple/peer/with_update_add_field_test.go b/tests/integration/net/simple/peer/with_update_add_field_test.go index 8c9d480cc3..22cfdbcb42 100644 --- a/tests/integration/net/simple/peer/with_update_add_field_test.go +++ b/tests/integration/net/simple/peer/with_update_add_field_test.go @@ -87,8 +87,11 @@ func TestP2PPeerUpdateWithNewFieldSyncsDocsToOlderCollectionVersionMultistep(t * }, }, }, - &action.Request{ - // The second update should still be received by the second node, updating Name + // The second update should still be received by the second node, + // updating Name. That node may be on an older release, which can hold + // the document without being able to report the commit that carried + // it, so there is nothing for WaitForSync to observe. Poll instead. + action.NewEventually(&action.Request{ NodeID: immutable.Some(1), Request: `query { Users { @@ -102,7 +105,7 @@ func TestP2PPeerUpdateWithNewFieldSyncsDocsToOlderCollectionVersionMultistep(t * }, }, }, - }, + }), }, } @@ -169,7 +172,11 @@ func TestP2PPeerUpdateWithNewFieldSyncsDocsToOlderCollectionVersion(t *testing.T }, }, }, - &action.Request{ + // The second node may be on an older release, which can hold this + // document without being able to report the commit that carried it. + // There is nothing for WaitForSync to observe in that case, so poll + // the result instead. + action.NewEventually(&action.Request{ NodeID: immutable.Some(1), Request: `query { Users { @@ -183,7 +190,7 @@ func TestP2PPeerUpdateWithNewFieldSyncsDocsToOlderCollectionVersion(t *testing.T }, }, }, - }, + }), }, } diff --git a/tests/integration/net/simple/peer/with_update_restart_test.go b/tests/integration/net/simple/peer/with_update_restart_test.go index d27a587d04..b5e289a3cb 100644 --- a/tests/integration/net/simple/peer/with_update_restart_test.go +++ b/tests/integration/net/simple/peer/with_update_restart_test.go @@ -18,11 +18,19 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestP2PWithSingleDocumentSingleUpdateFromChildAndRestart(t *testing.T) { test := testUtils.TestCase{ + // An external node is started as a new process with a new identity, so it + // cannot reopen the store it wrote before the restart. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/net/simple/peer_replicator/with_update_restart_test.go b/tests/integration/net/simple/peer_replicator/with_update_restart_test.go index bcfceaec0d..b68ef2f76e 100644 --- a/tests/integration/net/simple/peer_replicator/with_update_restart_test.go +++ b/tests/integration/net/simple/peer_replicator/with_update_restart_test.go @@ -18,11 +18,19 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestP2PPeerReplicatorWithUpdateAndRestart(t *testing.T) { test := testUtils.TestCase{ + // An external node is started as a new process with a new identity, so it + // cannot reopen the store it wrote before the restart. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/net/simple/replicator/with_add_restart_test.go b/tests/integration/net/simple/replicator/with_add_restart_test.go index 80834240b9..34cfe60f9a 100644 --- a/tests/integration/net/simple/replicator/with_add_restart_test.go +++ b/tests/integration/net/simple/replicator/with_add_restart_test.go @@ -18,10 +18,18 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" ) func TestP2POneToOneReplicatorWithRestart(t *testing.T) { test := testUtils.TestCase{ + // An external node is started as a new process with a new identity, so it + // cannot reopen the store it wrote before the restart. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/net/simple/replicator/with_add_test.go b/tests/integration/net/simple/replicator/with_add_test.go index 5b44dfedda..4ff8d11367 100644 --- a/tests/integration/net/simple/replicator/with_add_test.go +++ b/tests/integration/net/simple/replicator/with_add_test.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) @@ -593,6 +594,13 @@ func TestP2POneToOneReplicatorOrderIndependentDirectAdd(t *testing.T) { func TestP2POneToOneReplicator_ManyDocsWithTargetNodeTemporarilyOffline_ShouldSucceed(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDatabaseTypes: immutable.Some( []state.DatabaseType{ // This test only supports file type databases since it requires the ability to diff --git a/tests/integration/net/simple/replicator/with_update_add_field_test.go b/tests/integration/net/simple/replicator/with_update_add_field_test.go index 8425aac4ee..21c439a1c1 100644 --- a/tests/integration/net/simple/replicator/with_update_add_field_test.go +++ b/tests/integration/net/simple/replicator/with_update_add_field_test.go @@ -83,8 +83,11 @@ func TestP2PReplicatorUpdateWithNewFieldSyncsDocsToOlderCollectionVersionMultist }, }, }, - &action.Request{ - // The second update should still be received by the second node, updating Name + // The second update should still be received by the second node, + // updating Name. That node may be on an older release, which can hold + // the document without being able to report the commit that carried + // it, so there is nothing for WaitForSync to observe. Poll instead. + action.NewEventually(&action.Request{ NodeID: immutable.Some(1), Request: `query { Users { @@ -98,7 +101,7 @@ func TestP2PReplicatorUpdateWithNewFieldSyncsDocsToOlderCollectionVersionMultist }, }, }, - }, + }), }, } @@ -161,7 +164,11 @@ func TestP2PReplicatorUpdateWithNewFieldSyncsDocsToOlderCollectionVersion(t *tes }, }, }, - &action.Request{ + // The second node may be on an older release, which can hold this + // document without being able to report the commit that carried it. + // There is nothing for WaitForSync to observe in that case, so poll + // the result instead. + action.NewEventually(&action.Request{ NodeID: immutable.Some(1), Request: `query { Users { @@ -175,7 +182,7 @@ func TestP2PReplicatorUpdateWithNewFieldSyncsDocsToOlderCollectionVersion(t *tes }, }, }, - }, + }), }, } diff --git a/tests/integration/net/simple/replicator/with_update_test.go b/tests/integration/net/simple/replicator/with_update_test.go index fabfc57d7e..f46c5a24ed 100644 --- a/tests/integration/net/simple/replicator/with_update_test.go +++ b/tests/integration/net/simple/replicator/with_update_test.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) @@ -131,6 +132,13 @@ func TestP2POneToOneReplicatorUpdatesDocAddedBeforeReplicatorConfigWithNodesInve func TestP2POneToOneReplicator_ManyDocsUpdateWithTargetNodeTemporarilyOffline_ShouldSucceed(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDatabaseTypes: immutable.Some( []state.DatabaseType{ // This test only supports file type databases since it requires the ability to @@ -212,6 +220,13 @@ func TestP2POneToOneReplicator_ManyDocsUpdateWithTargetNodeTemporarilyOffline_Sh func TestP2POneToOneReplicator_ManyDocsUpdateWithTargetNodeTemporarilyOfflineAfterAdd_ShouldSucceed(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDatabaseTypes: immutable.Some( []state.DatabaseType{ // This test only supports file type databases since it requires the ability to diff --git a/tests/integration/node/identity_test.go b/tests/integration/node/identity_test.go index 2835f8926e..cd2fe90e8e 100644 --- a/tests/integration/node/identity_test.go +++ b/tests/integration/node/identity_test.go @@ -15,10 +15,18 @@ import ( "testing" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" ) func TestNodeIdentity_NodeIdentity_Succeed(t *testing.T) { test := testUtils.TestCase{ + // An external node generates its own node identity, so it does not match the + // identity the harness expects. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, Actions: []any{ testUtils.RandomNetworkingConfig(), testUtils.RandomNetworkingConfig(), diff --git a/tests/integration/searchable_encryption/replicator_test.go b/tests/integration/searchable_encryption/replicator_test.go index fd8d189c11..6179f7160b 100644 --- a/tests/integration/searchable_encryption/replicator_test.go +++ b/tests/integration/searchable_encryption/replicator_test.go @@ -20,11 +20,19 @@ import ( "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) func TestSEReplicator_IfDocAddedWhileReplicatorIsOffline_ShouldRetry(t *testing.T) { test := testUtils.TestCase{ + // Restarting a node re-creates the external node as a new process on a new + // port, and its auth token is still bound to the old address. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, EnableSearchableEncryption: true, SupportedDatabaseTypes: immutable.Some( []state.DatabaseType{ diff --git a/tests/integration/signature/peer_test.go b/tests/integration/signature/peer_test.go index c0cce6d98f..0677cff310 100644 --- a/tests/integration/signature/peer_test.go +++ b/tests/integration/signature/peer_test.go @@ -20,6 +20,7 @@ import ( coreblock "github.com/sourcenetwork/defradb/internal/core/block" "github.com/sourcenetwork/defradb/tests/action" testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/multiplier" "github.com/sourcenetwork/defradb/tests/state" ) @@ -139,6 +140,14 @@ func TestDocSignature_WithPeersAndEd25519KeyType_ShouldSync(t *testing.T) { func TestDocSignature_WithPeersAnDifferentKeyTypes_ShouldSync(t *testing.T) { test := testUtils.TestCase{ + // An external node generates its own node identity on every start, so the + // signatures it writes carry a key the test cannot predict. The key type is + // honoured, the identity is not. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, EnableSigning: true, IdentityTypes: map[state.Identity]crypto.KeyType{ testUtils.NodeIdentity(0).Value(): crypto.KeyTypeSecp256k1, @@ -239,6 +248,14 @@ func TestDocSignature_WithPeersAnDifferentKeyTypes_ShouldSync(t *testing.T) { func TestDocSignature_WithPeersAnDifferentKeyTypesUpdatingSameDoc_ShouldSync(t *testing.T) { test := testUtils.TestCase{ + // An external node generates its own node identity on every start, so the + // signatures it writes carry a key the test cannot predict. The key type is + // honoured, the identity is not. + // https://github.com/sourcenetwork/defradb/issues/5170 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, EnableSigning: true, IdentityTypes: map[state.Identity]crypto.KeyType{ testUtils.NodeIdentity(0).Value(): crypto.KeyTypeSecp256k1, diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 8fa6b585ef..e580a46489 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -224,7 +224,7 @@ func ExecuteTestCase( kms, dbt, ct, - documentACPType, + action.DocumentACPType, ) } @@ -801,6 +801,11 @@ func applyMultipliers(t testing.TB, testCase *TestCase) { defraMultiplier.SignedDocs) } + if name, ok := externalNodeMultiplierUnsupported(testCase, activeMultipliers); ok { + t.Skipf("test supports client types %v, but the %q multiplier runs a node over HTTP", + testCase.SupportedClientTypes.Value(), name) + } + modified := multiplier.Apply(actions) for i, idx := range actionIndices { @@ -810,6 +815,28 @@ func applyMultipliers(t testing.TB, testCase *TestCase) { applyTestCaseLevelMultipliers(testCase, activeMultipliers) } +// externalNodeMultiplierUnsupported reports whether an active multiplier would run +// a node the test cannot drive, and names it. +// +// A node in another process is reached over HTTP whatever the run-wide client type, +// so a test that lists its clients without HTTP cannot run under such a multiplier. +// Listing no clients means any client will do. +func externalNodeMultiplierUnsupported(testCase *TestCase, activeNames string) (string, bool) { + if !testCase.SupportedClientTypes.HasValue() || + slices.Contains(testCase.SupportedClientTypes.Value(), state.HTTPClientType) { + return "", false + } + + for name := range strings.SplitSeq(activeNames, ",") { + name = strings.TrimSpace(name) + if defraMultiplier.MakesNodeExternal(name) { + return name, true + } + } + + return "", false +} + // applyTestCaseLevelMultipliers mutates TestCase fields based on the given // comma-separated list of active multiplier names. // @@ -2300,14 +2327,14 @@ func skipIfDocumentACPTypeUnsupported(t testing.TB, supportedACPTypes immutable. if supportedACPTypes.HasValue() { var isTypeSupported bool for _, supportedType := range supportedACPTypes.Value() { - if supportedType == documentACPType { + if supportedType == action.DocumentACPType { isTypeSupported = true break } } if !isTypeSupported { - t.Skipf("test does not support given acp type. Type: %s", documentACPType) + t.Skipf("test does not support given acp type. Type: %s", action.DocumentACPType) } } } diff --git a/tests/multiplier/cross_version.go b/tests/multiplier/cross_version.go new file mode 100644 index 0000000000..8b461da7e1 --- /dev/null +++ b/tests/multiplier/cross_version.go @@ -0,0 +1,123 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package multiplier + +import ( + "github.com/sourcenetwork/testo/multiplier" + + "github.com/sourcenetwork/defradb/tests/action" +) + +func init() { + multiplier.Register(&crossVersion{name: CrossVersionOldSource, oldNodeFirst: true}) + multiplier.Register(&crossVersion{name: CrossVersionNewSource, oldNodeFirst: false}) +} + +// CrossVersionTargetVersion is the older release run against the current build. +// v1.0.0 is the only post-1.0 release, so it is the only pair for now. +const CrossVersionTargetVersion = "v1.0.0" + +// CrossVersionOldSource runs the first node on the older release, so data starts +// on the old node. +// +// Tests needing behaviour the older release lacks opt out with MultiplierExcludes. +// A version-aware gate is tracked in +// https://github.com/sourcenetwork/defradb/issues/5121 +const CrossVersionOldSource Name = "cross-version-old-source" + +// CrossVersionNewSource runs the last node on the older release, so data starts +// on the current build. +const CrossVersionNewSource Name = "cross-version-new-source" + +// crossVersion runs one node of a networked test on an older release, so the +// existing P2P suite also checks compatibility with that release. +// +// Both directions are worth running because they fail differently: a new node +// sending to an old one relies on the old node ignoring fields it does not know, +// and an old node sending to a new one relies on the new node reading a missing +// field as a zero value. Each direction is registered separately because a +// multiplier applies one transformation per run. +type crossVersion struct { + name Name + // oldNodeFirst picks which node carries the older version. Node 0 is the + // source in most of the suite, so this sets the direction. + oldNodeFirst bool +} + +var _ Multiplier = (*crossVersion)(nil) +var _ multiplier.ActionAwareSkipper = (*crossVersion)(nil) + +func (m *crossVersion) Name() Name { + return m.name +} + +// ShouldSkip implements [multiplier.ActionAwareSkipper]. +// +// A test with one node has nothing to check compatibility against, and a test +// that already sets a version is checking something specific that this would +// overwrite. +func (m *crossVersion) ShouldSkip(actions action.Actions) bool { + nodes := nodeActions(actions) + if len(nodes) < 2 { + return true + } + + for _, node := range nodes { + if node.Version != "" { + return true + } + } + + return false +} + +func (m *crossVersion) Apply(source action.Actions) action.Actions { + nodes := nodeActions(source) + if len(nodes) < 2 { + return source + } + + target := nodes[len(nodes)-1] + if m.oldNodeFirst { + target = nodes[0] + } + + result := make(action.Actions, len(source)) + for i, a := range source { + if cfg, ok := a.(*action.NewNode); ok && cfg == target { + result[i] = cfg.WithVersion(CrossVersionTargetVersion) + continue + } + result[i] = a + } + + return result +} + +// nodeActions returns the node creation actions in the action set, in order. +func nodeActions(actions action.Actions) []*action.NewNode { + var configs []*action.NewNode + for _, a := range actions { + if cfg, ok := a.(*action.NewNode); ok { + configs = append(configs, cfg) + } + } + return configs +} + +// MakesNodeExternal reports whether the named multiplier runs one of the nodes as a +// separate process. +// +// Such a node is reached over HTTP whatever the run-wide client type. +func MakesNodeExternal(name Name) bool { + return name == CrossVersionOldSource || name == CrossVersionNewSource +} diff --git a/tests/multiplier/cross_version_test.go b/tests/multiplier/cross_version_test.go new file mode 100644 index 0000000000..c504b0080d --- /dev/null +++ b/tests/multiplier/cross_version_test.go @@ -0,0 +1,241 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package multiplier + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + m "github.com/sourcenetwork/testo/multiplier" + + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/tests/action" +) + +// nodeAt returns the node action at index i, failing the test if the action is +// not a node. Keeps the assertions checked rather than panicking on a bad index. +func nodeAt(t *testing.T, actions action.Actions, i int) *action.NewNode { + t.Helper() + require.Greater(t, len(actions), i, "no action at index %d", i) + node, ok := actions[i].(*action.NewNode) + require.True(t, ok, "action %d is %T, not a node", i, actions[i]) + return node +} + +func oldSource() *crossVersion { + return &crossVersion{name: CrossVersionOldSource, oldNodeFirst: true} +} + +func newSource() *crossVersion { + return &crossVersion{name: CrossVersionNewSource, oldNodeFirst: false} +} + +func TestCrossVersionNames_Stable(t *testing.T) { + // The names are part of the CI contract, used in DEFRA_MULTIPLIERS and in + // MultiplierExcludes. Changing them breaks workflow config and every test + // that opts out. + assert.Equal(t, "cross-version-old-source", string(CrossVersionOldSource)) + assert.Equal(t, "cross-version-new-source", string(CrossVersionNewSource)) + assert.Equal(t, CrossVersionOldSource, oldSource().Name()) + assert.Equal(t, CrossVersionNewSource, newSource().Name()) +} + +func TestCrossVersion_ImplementsInterfaces(t *testing.T) { + var _ Multiplier = (*crossVersion)(nil) + var _ m.Multiplier = (*crossVersion)(nil) + var _ m.ActionAwareSkipper = (*crossVersion)(nil) +} + +func TestCrossVersion_IsRegistered(t *testing.T) { + m.Init("__cross_version_test_unset_env__", CrossVersionOldSource, CrossVersionNewSource) + t.Cleanup(func() { + m.Init("__cross_version_test_unset_env__") + }) + + active := m.Get() + assert.Contains(t, active, string(CrossVersionOldSource)) + assert.Contains(t, active, string(CrossVersionNewSource)) +} + +func TestCrossVersionApply_WithEmptyActions_ReturnsEmpty(t *testing.T) { + result := oldSource().Apply(action.Actions{}) + + assert.Empty(t, result) +} + +func TestCrossVersionApply_WithNilActions_ReturnsNil(t *testing.T) { + result := oldSource().Apply(nil) + + assert.Nil(t, result) +} + +func TestCrossVersionApply_OldSource_VersionsFirstNode(t *testing.T) { + first := action.RandomNetworkingConfig() + second := action.RandomNetworkingConfig() + source := action.Actions{first, second} + + result := oldSource().Apply(source) + + require.Len(t, result, 2) + assert.Equal(t, CrossVersionTargetVersion, nodeAt(t, result, 0).Version) + assert.Equal(t, "", nodeAt(t, result, 1).Version) +} + +func TestCrossVersionApply_NewSource_VersionsLastNode(t *testing.T) { + first := action.RandomNetworkingConfig() + second := action.RandomNetworkingConfig() + source := action.Actions{first, second} + + result := newSource().Apply(source) + + require.Len(t, result, 2) + assert.Equal(t, "", nodeAt(t, result, 0).Version) + assert.Equal(t, CrossVersionTargetVersion, nodeAt(t, result, 1).Version) +} + +func TestCrossVersionApply_WithThreeNodes_VersionsOnlyOne(t *testing.T) { + source := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + } + + result := newSource().Apply(source) + + require.Len(t, result, 3) + assert.Equal(t, "", nodeAt(t, result, 0).Version) + assert.Equal(t, "", nodeAt(t, result, 1).Version) + assert.Equal(t, CrossVersionTargetVersion, nodeAt(t, result, 2).Version) +} + +func TestCrossVersionApply_LeavesOtherActionsUntouched(t *testing.T) { + add := &action.AddCollection{SDL: "type User { name: String }"} + source := action.Actions{ + action.RandomNetworkingConfig(), + add, + action.RandomNetworkingConfig(), + } + + result := oldSource().Apply(source) + + require.Len(t, result, 3) + assert.Same(t, add, result[1], "non node-config actions must not be replaced") +} + +func TestCrossVersionApply_DoesNotMutateSource(t *testing.T) { + // Apply must not write through to the caller's config, otherwise a test run + // would leak the version into the next multiplier or a later run. + first := action.RandomNetworkingConfig() + source := action.Actions{first, action.RandomNetworkingConfig()} + + oldSource().Apply(source) + + assert.Equal(t, "", first.Version, "the original config must be unchanged") +} + +func TestCrossVersionApply_PreservesNetworkingConfig(t *testing.T) { + source := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + } + + result := oldSource().Apply(source) + + versioned := nodeAt(t, result, 0) + assert.NotNil(t, versioned.Network, "networking config must survive the rewrite") +} + +func TestCrossVersionApply_WithSingleNode_ReturnsSourceUnchanged(t *testing.T) { + source := action.Actions{action.RandomNetworkingConfig()} + + result := oldSource().Apply(source) + + assert.Equal(t, "", nodeAt(t, result, 0).Version) +} + +func TestCrossVersionShouldSkip_WithSingleNode_Skips(t *testing.T) { + // A single node has nothing to check compatibility against. + actions := action.Actions{action.RandomNetworkingConfig()} + + assert.True(t, oldSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithNoNodes_Skips(t *testing.T) { + actions := action.Actions{&action.AddCollection{SDL: "type User { name: String }"}} + + assert.True(t, oldSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithTwoNodes_DoesNotSkip(t *testing.T) { + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + } + + assert.False(t, oldSource().ShouldSkip(actions)) + assert.False(t, newSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithWritesNamingTheirNode_DoesNotSkip(t *testing.T) { + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + &action.AddDoc{NodeID: immutable.Some(0), Doc: `{"Name": "John"}`}, + &action.UpdateDoc{NodeID: immutable.Some(0), Doc: `{"Name": "Fred"}`}, + } + + assert.False(t, oldSource().ShouldSkip(actions)) + assert.False(t, newSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithWritesNotNamingTheirNode_DoesNotSkip(t *testing.T) { + // A write with no node set lands on the versioned node too. That is still + // worth running: what the node received is checked by querying it, so the + // write does not need to say where it went. + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + &action.AddDoc{Doc: `{"Name": "John"}`}, + &action.UpdateDoc{Doc: `{"Name": "Fred"}`}, + } + + assert.False(t, oldSource().ShouldSkip(actions)) + assert.False(t, newSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithVersionAlreadySet_Skips(t *testing.T) { + // The hand written cross version tests pin their own versions. Rewriting + // them would test something other than what they were written to check. + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig().WithVersion("v1.0.0"), + } + + assert.True(t, oldSource().ShouldSkip(actions)) +} + +func TestMakesNodeExternal_WithCrossVersionMultipliers_ReturnsTrue(t *testing.T) { + assert.True(t, MakesNodeExternal(CrossVersionOldSource)) + assert.True(t, MakesNodeExternal(CrossVersionNewSource)) +} + +func TestMakesNodeExternal_WithOtherMultipliers_ReturnsFalse(t *testing.T) { + // These run every node in process, so a test's supported client types still hold. + assert.False(t, MakesNodeExternal(SignedDocs)) + assert.False(t, MakesNodeExternal(SecondaryIndex)) + assert.False(t, MakesNodeExternal(EncryptedDocs)) + assert.False(t, MakesNodeExternal(Name(""))) + assert.False(t, MakesNodeExternal(Name("not-a-multiplier"))) +} diff --git a/tests/state/identity.go b/tests/state/identity.go index c653d4cf16..4fac19342a 100644 --- a/tests/state/identity.go +++ b/tests/state/identity.go @@ -14,6 +14,7 @@ package state import ( "crypto/ed25519" "encoding/base64" + "encoding/json" "math/rand" "strings" @@ -104,10 +105,13 @@ func GetIdentityHolder(s *State, identity Identity) *IdentityHolder { return s.Identities[identity] } -// TokenHasAudience returns true if the given JWT token string contains an audience claim. -// This is used to detect tokens that were generated before the node's HTTP host was available, -// and need to be regenerated with the correct audience. -func TokenHasAudience(token string) bool { +// TokenHasAudience returns true if the given JWT token carries the given audience. +// +// It detects both a token generated before the node's HTTP host was available and +// one generated for a different host. An external node binds a new port every start, +// so a token minted for an earlier address is rejected by that node and has to be +// regenerated. +func TokenHasAudience(token string, audience string) bool { if token == "" { return false } @@ -119,7 +123,26 @@ func TokenHasAudience(token string) bool { if err != nil { return false } - return strings.Contains(string(payload), `"aud"`) + + var claims struct { + Audience any `json:"aud"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return false + } + + // The audience claim is either a single string or a list of them. + switch aud := claims.Audience.(type) { + case string: + return aud == audience + case []any: + for _, a := range aud { + if s, ok := a.(string); ok && s == audience { + return true + } + } + } + return false } // Generate the keys using predefined seed so that multiple runs yield the same private key. diff --git a/tests/state/identity_test.go b/tests/state/identity_test.go new file mode 100644 index 0000000000..7f9eca1666 --- /dev/null +++ b/tests/state/identity_test.go @@ -0,0 +1,69 @@ +// Copyright 2026 Democratized Data Foundation +// +// This file is part of the DefraDB test suite. +// +// The DefraDB test suite is licensed under either: +// +// (1) GNU Affero General Public License v3 +// (2) Business Source License 1.1 +// +// See tests/LICENSE for details. + +package state + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// tokenWithClaims builds a JWT-shaped string carrying the given claims. Only the +// payload is read, so the header and signature are placeholders. +func tokenWithClaims(t *testing.T, claims map[string]any) string { + t.Helper() + payload, err := json.Marshal(claims) + require.NoError(t, err) + return "header." + base64.RawURLEncoding.EncodeToString(payload) + ".signature" +} + +func TestTokenHasAudience_WithMatchingAudience_ReturnsTrue(t *testing.T) { + token := tokenWithClaims(t, map[string]any{"aud": "127.0.0.1:9181"}) + + assert.True(t, TokenHasAudience(token, "127.0.0.1:9181")) +} + +func TestTokenHasAudience_WithStaleAudience_ReturnsFalse(t *testing.T) { + // An external node binds a new port every start. A token minted for the old + // address still carries an audience, so checking only that one is present + // leaves the stale token in place and the node answers 403. + token := tokenWithClaims(t, map[string]any{"aud": "127.0.0.1:39267"}) + + assert.False(t, TokenHasAudience(token, "127.0.0.1:45123")) +} + +func TestTokenHasAudience_WithNoAudienceClaim_ReturnsFalse(t *testing.T) { + token := tokenWithClaims(t, map[string]any{"sub": "did:key:z6Mk"}) + + assert.False(t, TokenHasAudience(token, "127.0.0.1:9181")) +} + +func TestTokenHasAudience_WithAudienceList_MatchesAnyEntry(t *testing.T) { + // The audience claim is allowed to be a list. + token := tokenWithClaims(t, map[string]any{ + "aud": []string{"127.0.0.1:9181", "127.0.0.1:45123"}, + }) + + assert.True(t, TokenHasAudience(token, "127.0.0.1:45123")) + assert.False(t, TokenHasAudience(token, "127.0.0.1:39267")) +} + +func TestTokenHasAudience_WithMalformedToken_ReturnsFalse(t *testing.T) { + assert.False(t, TokenHasAudience("", "127.0.0.1:9181")) + assert.False(t, TokenHasAudience("not-a-jwt", "127.0.0.1:9181")) + assert.False(t, TokenHasAudience("header.!!!not-base64!!!.sig", "127.0.0.1:9181")) + assert.False(t, TokenHasAudience("header."+ + base64.RawURLEncoding.EncodeToString([]byte("not json"))+".sig", "127.0.0.1:9181")) +} diff --git a/tests/state/state.go b/tests/state/state.go index 493a71690d..edb36a98d2 100644 --- a/tests/state/state.go +++ b/tests/state/state.go @@ -375,7 +375,18 @@ type State struct { SkipTest string } +// GetClientType returns the client type used to reach the node currently being +// asserted. +// +// A node running in another process is always reached over HTTP, whatever client +// the run selected, so its results need the same relaxed comparison the HTTP +// client gets. Reporting the run-wide type here would compare its values +// strictly and fail on equal values of a different Go type. func (s *State) GetClientType() ClientType { + nodeID := s.CurrentAssertingNodeID + if nodeID >= 0 && nodeID < len(s.Nodes) && s.Nodes[nodeID].IsExternal { + return HTTPClientType + } return s.ClientType }