From 397d3314cfa930bc97eb742c91f3105b09ae237b Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 4 Aug 2026 17:58:18 +0200 Subject: [PATCH 01/24] test(i): Generalize per-node test config into a struct Setting up a test node took a function that returned networking options, so networking was the only thing a test could say about a node. When nodes needed to run from an older release, the version had nowhere to go and became its own separate action that the harness handled alongside the first one. Both are replaced by a single struct holding the version and the networking config as ordinary fields. The function that builds a random networking config now returns that struct, so the several hundred places that use it are unchanged. The two tests that asked for an older node were updated by hand. The harness now looks for one thing instead of two in four places, and the two setup functions became one that checks whether a version was given. Starting a node is untouched. --- tests/integration/db.go | 2 +- .../simple/replicator/cross_version_test.go | 4 +- tests/integration/p2p_config.go | 18 ++--- tests/integration/p2p_config_js.go | 8 ++- tests/integration/test_case.go | 46 ++++++++----- tests/integration/utils.go | 68 ++++++------------- 6 files changed, 70 insertions(+), 76 deletions(-) diff --git a/tests/integration/db.go b/tests/integration/db.go index 49e9dd58d1..d77dd8c000 100644 --- a/tests/integration/db.go +++ b/tests/integration/db.go @@ -84,7 +84,7 @@ func defaultNodeOpts() *options.NodeOptionsBuilder { // The test framework sets this up elsewhere when required so that it may be wrapped // into a [client.TxnStore]. SetDisableAPI(true). - // The p2p is configured in the tests by [ConfigureNode] actions, we disable it here + // The p2p is configured in the tests by [NodeConfig] actions, we disable it here // to keep the tests as lightweight as possible. SetDisableP2P(true) diff --git a/tests/integration/net/simple/replicator/cross_version_test.go b/tests/integration/net/simple/replicator/cross_version_test.go index 77c7554828..34b303be24 100644 --- a/tests/integration/net/simple/replicator/cross_version_test.go +++ b/tests/integration/net/simple/replicator/cross_version_test.go @@ -76,7 +76,7 @@ func TestP2PCrossVersion_HeadToV1_DocSyncs(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.RandomNetworkingConfig(), // node 0 = HEAD - testUtils.NodeVersion{Version: crossVersion, Config: testUtils.RandomNetworkingConfig()}, // node 1 = v1.0.0 + testUtils.RandomNetworkingConfig().WithVersion(crossVersion), // node 1 = v1.0.0 &action.AddCollection{ SDL: ` type User { @@ -114,7 +114,7 @@ func TestP2PCrossVersion_V1ToHead_DocSyncs(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.RandomNetworkingConfig(), // node 0 = HEAD - testUtils.NodeVersion{Version: crossVersion, Config: testUtils.RandomNetworkingConfig()}, // node 1 = v1.0.0 + testUtils.RandomNetworkingConfig().WithVersion(crossVersion), // node 1 = v1.0.0 &action.AddCollection{ SDL: ` type User { diff --git a/tests/integration/p2p_config.go b/tests/integration/p2p_config.go index c98ba05733..0c0614d0f8 100644 --- a/tests/integration/p2p_config.go +++ b/tests/integration/p2p_config.go @@ -19,14 +19,16 @@ import ( "github.com/sourcenetwork/defradb/client/options" ) -func RandomNetworkingConfig() ConfigureNode { - return func() options.NodeP2POptions { - return options.NodeP2POptions{ - ListenAddresses: []string{"/ip4/" + getIPString() + "/tcp/0"}, - EnablePubSub: true, - EnableRelay: true, - EnableClearBackoffOnRetry: true, - } +func RandomNetworkingConfig() NodeConfig { + return NodeConfig{ + Network: func() options.NodeP2POptions { + return options.NodeP2POptions{ + ListenAddresses: []string{"/ip4/" + getIPString() + "/tcp/0"}, + EnablePubSub: true, + EnableRelay: true, + EnableClearBackoffOnRetry: true, + } + }, } } diff --git a/tests/integration/p2p_config_js.go b/tests/integration/p2p_config_js.go index 7e9d155055..2ddfa9786c 100644 --- a/tests/integration/p2p_config_js.go +++ b/tests/integration/p2p_config_js.go @@ -15,9 +15,11 @@ import ( "github.com/sourcenetwork/defradb/client/options" ) -func RandomNetworkingConfig() ConfigureNode { - return func() options.NodeP2POptions { - return options.NodeP2POptions{} +func RandomNetworkingConfig() NodeConfig { + return NodeConfig{ + Network: func() options.NodeP2POptions { + return options.NodeP2POptions{} + }, } } diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index 6cb8f7d877..e277d02606 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -121,7 +121,11 @@ type KMS struct { // the first item that is neither an AddCollection, AddDoc or UpdateDoc action. type SetupComplete struct{} -// ConfigureNode allows the explicit configuration of new Defra nodes. +// ConfigureNode returns the P2P options for a new Defra node. +type ConfigureNode func() options.NodeP2POptions + +// NodeConfig allows the explicit configuration of new Defra nodes. The zero value +// is a native, current-build node with default networking. // // If no nodes are explicitly configured, a default one will be setup. There is no // upper limit to the number that can be configured. @@ -129,24 +133,34 @@ type SetupComplete struct{} // Nodes may be explicitly referenced by index by other actions using `NodeID` properties. // If the action has a `NodeID` property and it is not specified, the action will be // effected on all nodes. -type ConfigureNode func() options.NodeP2POptions - -// NodeVersion configures a new node that runs as an external process from a -// published release binary of the given version, instead of natively in-process. -// It carries the same networking config a ConfigureNode would. // -// Version is plain data (not a closure) so a future multiplier can rewrite it to -// run existing tests against other versions. +// Configuration is held as plain data wherever possible so a future multiplier can +// rewrite it to run existing tests under other node configurations. // -// This lives here beside ConfigureNode rather than in the tests/action package -// because node creation is entangled with this package's setup path, which -// tests/action cannot import. When ConfigureNode migrates to tests/action, this -// should move with it. -type NodeVersion struct { - // Version names a published release, e.g. "v1.0.0". +// This lives here rather than in the tests/action package because node creation is +// entangled with this package's setup path, which tests/action cannot import. +type NodeConfig struct { + // Version, when set (e.g. "v1.0.0"), runs the node as an external process from + // that published release binary instead of natively in-process. Version string - // Config supplies the same networking config as a ConfigureNode. - Config ConfigureNode + // Network returns the node's P2P options. Nil means default networking. + Network ConfigureNode +} + +// P2POptions returns the configured P2P options, or the defaults if no networking +// config was supplied. +func (cfg NodeConfig) P2POptions() options.NodeP2POptions { + if cfg.Network == nil { + return options.NodeP2POptions{} + } + return cfg.Network() +} + +// WithVersion returns a copy of the config that runs the node as an external process +// from the given published release, e.g. "v1.0.0". +func (cfg NodeConfig) WithVersion(version string) NodeConfig { + cfg.Version = version + return cfg } func applyHTTPOptions(opts *options.NodeOptionsBuilder, httpOpts options.NodeHTTPOptions) { diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 187337b7f9..a98da355eb 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -362,12 +362,9 @@ func performAction( case action.Action: action.Execute() - case ConfigureNode: + case NodeConfig: configureNode(s, testCase, action) - case NodeVersion: - configureNodeVersion(s, testCase, action) - case Restart: restartNodes(s, testCase) @@ -838,7 +835,7 @@ func createsDocsOnMultipleNodes(testCase *TestCase) bool { nodeCount := 0 for _, a := range testCase.Actions { switch a.(type) { - case ConfigureNode, NodeVersion: + case NodeConfig: nodeCount++ } } @@ -997,7 +994,7 @@ func actionTransactionID(a any) (int, bool) { // setStartingNodes adds a set of initial Defra nodes for the test to execute against. // -// If a node(s) has been explicitly configured via a `ConfigureNode` action then no new +// If a node(s) has been explicitly configured via a `NodeConfig` action then no new // nodes will be added. func setStartingNodes( s *state.State, @@ -1005,7 +1002,7 @@ func setStartingNodes( ) { for _, action := range testCase.Actions { switch action.(type) { - case ConfigureNode, NodeVersion: + case NodeConfig: s.IsNetworkEnabled = true } } @@ -1253,12 +1250,11 @@ func refreshCollections( // configureNode configures and starts a new Defra node using the provided configuration. // -// It returns the new node, and its peer address. Any errors generated during configuration -// will result in a test failure. +// Any errors generated during configuration will result in a test failure. func configureNode( s *state.State, testCase TestCase, - action ConfigureNode, + cfg NodeConfig, ) { if changeDetector.Enabled { // We do not yet support the change detector for tests running across multiple nodes. @@ -1266,45 +1262,25 @@ func configureNode( return } - privateKey, err := crypto.GenerateEd25519() - require.NoError(s.T, err) - - p2pOpts := action() - withPrivateKey(&p2pOpts, privateKey) - + p2pOpts := cfg.P2POptions() s.CurrentSetupNodeID = len(s.Nodes) - opts := defaultNodeOpts() - opts.DB(). - SetRetryIntervals([]time.Duration{time.Millisecond * 1}). - SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) - opts.P2P().SetAll(p2pOpts) - - node, err := setupNode(s, acpIdentity.None, testCase, opts, "") - require.NoError(s.T, err) - node.P2POpts = p2pOpts - s.Nodes = append(s.Nodes, node) -} + // Versioned nodes run in a separate process from a release binary that configures + // itself, so in-process options do not apply to them. + var opts *options.NodeOptionsBuilder + if cfg.Version == "" { + privateKey, err := crypto.GenerateEd25519() + require.NoError(s.T, err) + withPrivateKey(&p2pOpts, privateKey) -// configureNodeVersion configures and starts a new Defra node that runs as -// an external process from a downloaded release binary of the given version, -// instead of natively in-process. -func configureNodeVersion( - s *state.State, - testCase TestCase, - action NodeVersion, -) { - if changeDetector.Enabled { - // We do not yet support the change detector for tests running across multiple nodes. - s.T.SkipNow() - return + opts = defaultNodeOpts() + opts.DB(). + SetRetryIntervals([]time.Duration{time.Millisecond * 1}). + SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) + opts.P2P().SetAll(p2pOpts) } - p2pOpts := action.Config() - - s.CurrentSetupNodeID = len(s.Nodes) - - node, err := setupNode(s, acpIdentity.None, testCase, nil, action.Version) + node, err := setupNode(s, acpIdentity.None, testCase, opts, cfg.Version) require.NoError(s.T, err) if node == nil { // setupNode already skipped the test (no release asset for this platform). @@ -1312,7 +1288,7 @@ func configureNodeVersion( } node.P2POpts = p2pOpts - node.Version = action.Version + node.Version = cfg.Version s.Nodes = append(s.Nodes, node) } @@ -2406,7 +2382,7 @@ func skipIfNetworkTest(t testing.TB, actions []any) { hasNetworkAction := false for _, act := range actions { switch act.(type) { - case ConfigureNode, NodeVersion: + case NodeConfig: hasNetworkAction = true } } From 9dec1839242a6e87f1cd03c4d33d43dd7c80efe1 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 4 Aug 2026 17:58:18 +0200 Subject: [PATCH 02/24] test(i): Generalize per-node test config into a struct Setting up a test node took a function that returned networking options, so networking was the only thing a test could say about a node. When nodes needed to run from an older release, the version had nowhere to go and became its own separate action that the harness handled alongside the first one. Both are replaced by a single struct holding the version and the networking config as ordinary fields. The function that builds a random networking config now returns that struct, so the several hundred places that use it are unchanged. The two tests that asked for an older node were updated by hand. The harness now looks for one thing instead of two in four places, and the two setup functions became one that checks whether a version was given. Starting a node is untouched. --- tests/integration/net/simple/replicator/cross_version_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/net/simple/replicator/cross_version_test.go b/tests/integration/net/simple/replicator/cross_version_test.go index 34b303be24..9bf503ede2 100644 --- a/tests/integration/net/simple/replicator/cross_version_test.go +++ b/tests/integration/net/simple/replicator/cross_version_test.go @@ -75,7 +75,7 @@ func hasUserWithName(data any, name string) bool { func TestP2PCrossVersion_HeadToV1_DocSyncs(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ - testUtils.RandomNetworkingConfig(), // node 0 = HEAD + testUtils.RandomNetworkingConfig(), // node 0 = HEAD testUtils.RandomNetworkingConfig().WithVersion(crossVersion), // node 1 = v1.0.0 &action.AddCollection{ SDL: ` @@ -113,7 +113,7 @@ func TestP2PCrossVersion_HeadToV1_DocSyncs(t *testing.T) { func TestP2PCrossVersion_V1ToHead_DocSyncs(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ - testUtils.RandomNetworkingConfig(), // node 0 = HEAD + testUtils.RandomNetworkingConfig(), // node 0 = HEAD testUtils.RandomNetworkingConfig().WithVersion(crossVersion), // node 1 = v1.0.0 &action.AddCollection{ SDL: ` From c5a906493e6977165dd705c388f0f904e1265301 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 4 Aug 2026 23:46:46 +0200 Subject: [PATCH 03/24] Move node setup to action package --- tests/action/acp_dac_config.go | 45 +++++ .../{integration => action}/acp_dac_setup.go | 17 +- .../acp_dac_setup_js.go | 5 +- tests/action/client_setup.go | 64 +++++++ tests/action/client_setup_js.go | 26 +++ tests/action/node_config.go | 115 ++++++++++++ tests/action/node_config_test.go | 144 +++++++++++++++ tests/action/node_options.go | 169 ++++++++++++++++++ .../db_setup.go => action/node_setup.go} | 24 +-- .../node_setup_js.go} | 18 +- tests/{integration => action}/p2p_config.go | 10 +- .../{integration => action}/p2p_config_js.go | 10 +- tests/integration/acp_dac.go | 27 +-- tests/integration/client_setup.go | 50 ------ tests/integration/client_setup_js.go | 14 -- tests/integration/db.go | 84 +-------- tests/integration/lens.go | 14 -- tests/integration/test_case.go | 94 +++------- tests/integration/utils.go | 120 ++++--------- 19 files changed, 671 insertions(+), 379 deletions(-) create mode 100644 tests/action/acp_dac_config.go rename tests/{integration => action}/acp_dac_setup.go (93%) rename tests/{integration => action}/acp_dac_setup_js.go (67%) create mode 100644 tests/action/client_setup.go create mode 100644 tests/action/client_setup_js.go create mode 100644 tests/action/node_config.go create mode 100644 tests/action/node_config_test.go create mode 100644 tests/action/node_options.go rename tests/{integration/db_setup.go => action/node_setup.go} (95%) rename tests/{integration/db_setup_js.go => action/node_setup_js.go} (88%) rename tests/{integration => action}/p2p_config.go (86%) rename tests/{integration => action}/p2p_config_js.go (71%) diff --git a/tests/action/acp_dac_config.go b/tests/action/acp_dac_config.go new file mode 100644 index 0000000000..edd7a09e14 --- /dev/null +++ b/tests/action/acp_dac_config.go @@ -0,0 +1,45 @@ +// 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" + sourcehubImageEnvName = "DEFRA_SOURCEHUB_IMAGE" +) + +var ( + // 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. + DocumentACPType state.DocumentACPType + + // sourcehubImage is the container image used to run SourceHub. + sourcehubImage string +) + +func init() { + DocumentACPType = state.DocumentACPType(os.Getenv(documentACPTypeEnvName)) + if DocumentACPType == "" { + DocumentACPType = state.LocalDocumentACPType + } + sourcehubImage = os.Getenv(sourcehubImageEnvName) + if sourcehubImage == "" { + sourcehubImage = "ghcr.io/sourcenetwork/sourcehub:dev" + } +} diff --git a/tests/integration/acp_dac_setup.go b/tests/action/acp_dac_setup.go similarity index 93% rename from tests/integration/acp_dac_setup.go rename to tests/action/acp_dac_setup.go index f076f6a508..235e907877 100644 --- a/tests/integration/acp_dac_setup.go +++ b/tests/action/acp_dac_setup.go @@ -11,7 +11,7 @@ //go:build !js -package tests +package action import ( "bytes" @@ -37,7 +37,6 @@ import ( "github.com/sourcenetwork/defradb/client/options" "github.com/sourcenetwork/defradb/keyring" - "github.com/sourcenetwork/defradb/tests/action" "github.com/sourcenetwork/defradb/tests/state" "github.com/sourcenetwork/sourcehub/sdk" ) @@ -53,19 +52,7 @@ const ( sourcehubTestChainID string = "sourcehub-dev" ) -func setupSourceHub(s *state.State, testCase TestCase) (*options.NodeDocumentACPOptions, error) { - var isDocumentACPTest bool - for _, a := range testCase.Actions { - switch a.(type) { - case - AddDACPolicy, - AddDACActorRelationship, - *action.AddDACCollectionActorRelationship, - DeleteDACActorRelationship: - isDocumentACPTest = true - } - } - +func setupSourceHub(s *state.State, isDocumentACPTest bool) (*options.NodeDocumentACPOptions, error) { if !isDocumentACPTest { // Spinning up SourceHub instances is a bit slow, so we should be quite aggressive in trimming down the // runtime of the test suite when SourceHub ACP is selected. diff --git a/tests/integration/acp_dac_setup_js.go b/tests/action/acp_dac_setup_js.go similarity index 67% rename from tests/integration/acp_dac_setup_js.go rename to tests/action/acp_dac_setup_js.go index 1e6c63439d..0bf958530c 100644 --- a/tests/integration/acp_dac_setup_js.go +++ b/tests/action/acp_dac_setup_js.go @@ -9,13 +9,14 @@ // // See tests/LICENSE for details. -package tests +package action import ( "github.com/sourcenetwork/defradb/client/options" "github.com/sourcenetwork/defradb/tests/state" ) -func setupSourceHub(s *state.State) (*options.NodeDocumentACPOptions, error) { +// isDocumentACPTest is unused: the js build has no SourceHub container to gate on. +func setupSourceHub(s *state.State, isDocumentACPTest bool) (*options.NodeDocumentACPOptions, error) { return s.DocumentACPOptions, nil } diff --git a/tests/action/client_setup.go b/tests/action/client_setup.go new file mode 100644 index 0000000000..f341338867 --- /dev/null +++ b/tests/action/client_setup.go @@ -0,0 +1,64 @@ +// 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. + +//go:build !js + +package action + +import ( + "context" + "fmt" + + cbindings "github.com/sourcenetwork/defradb/cbindings" + "github.com/sourcenetwork/defradb/node" + "github.com/sourcenetwork/defradb/tests/clients" + "github.com/sourcenetwork/defradb/tests/clients/cli" + "github.com/sourcenetwork/defradb/tests/clients/http" + "github.com/sourcenetwork/defradb/tests/state" +) + +// setupClient returns the client implementation for the current +// testing state. The client type on the test state is used to +// select the client implementation to use. +func setupClient(s *state.State, nodeObj *node.Node) (clients.Client, error) { + switch s.ClientType { + case state.HTTPClientType: + return http.NewWrapper(nodeObj) + + case state.CLIClientType: + return cli.NewWrapper(nodeObj, s.SourcehubAddress) + + case state.GoClientType: + return newGoClientWrapper(nodeObj), nil + + case state.CClientType: + return cbindings.NewCWrapper(nodeObj) + + default: + return nil, fmt.Errorf("invalid client type: %v", s.ClientType) + } +} + +type goClientWrapper struct { + node.DB + node *node.Node +} + +func newGoClientWrapper(n *node.Node) *goClientWrapper { + return &goClientWrapper{ + DB: n.DB, + node: n, + } +} + +func (w *goClientWrapper) Close() { + _ = w.node.Close(context.Background()) +} diff --git a/tests/action/client_setup_js.go b/tests/action/client_setup_js.go new file mode 100644 index 0000000000..4a7ce42a23 --- /dev/null +++ b/tests/action/client_setup_js.go @@ -0,0 +1,26 @@ +// 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 ( + "github.com/sourcenetwork/defradb/node" + "github.com/sourcenetwork/defradb/tests/clients" + "github.com/sourcenetwork/defradb/tests/clients/js" + "github.com/sourcenetwork/defradb/tests/state" +) + +// setupClient returns the client implementation for the current +// testing state. The client type on the test state is used to +// select the client implementation to use. +func setupClient(_ *state.State, node *node.Node) (impl clients.Client, err error) { + return js.NewWrapper(node) +} diff --git a/tests/action/node_config.go b/tests/action/node_config.go new file mode 100644 index 0000000000..521b203120 --- /dev/null +++ b/tests/action/node_config.go @@ -0,0 +1,115 @@ +// 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 ( + "time" + + "github.com/stretchr/testify/require" + + acpIdentity "github.com/sourcenetwork/defradb/acp/identity" + "github.com/sourcenetwork/defradb/client/options" + "github.com/sourcenetwork/defradb/crypto" + changeDetector "github.com/sourcenetwork/defradb/tests/change_detector" + "github.com/sourcenetwork/defradb/tests/state" +) + +// ConfigureNode returns the P2P options for a new Defra node. +type ConfigureNode func() options.NodeP2POptions + +// NodeConfig allows the explicit configuration of new Defra nodes. The zero value +// is a native, current-build node with default networking. +// +// If no nodes are explicitly configured, a default one will be setup. There is no +// upper limit to the number that can be configured. +// +// Nodes may be explicitly referenced by index by other actions using `NodeID` properties. +// If the action has a `NodeID` property and it is not specified, the action will be +// effected on all nodes. +// +// Configuration is held as plain data so a multiplier can rewrite it to run existing +// tests under other node configurations. +type NodeConfig struct { + stateful + + // Version, when set (e.g. "v1.0.0"), runs the node as an external process from + // that published release binary instead of natively in-process. + Version string + // Network returns the node's P2P options. Nil means default networking. + Network ConfigureNode + + // SetupConfig carries the test-level settings node setup needs. The harness + // sets it before execution. + SetupConfig NodeSetupConfig +} + +var _ Action = (*NodeConfig)(nil) +var _ Stateful = (*NodeConfig)(nil) + +// P2POptions returns the configured P2P options, or the defaults if no networking +// config was supplied. +func (a *NodeConfig) P2POptions() options.NodeP2POptions { + if a.Network == nil { + return options.NodeP2POptions{} + } + return a.Network() +} + +// WithVersion returns a copy of the config that runs the node as an external process +// from the given published release, e.g. "v1.0.0". +func (a *NodeConfig) WithVersion(version string) *NodeConfig { + clone := *a + clone.Version = version + return &clone +} + +// Execute configures and starts a new Defra node. +// +// Any errors generated during configuration will result in a test failure. +func (a *NodeConfig) Execute() { + s := a.s + + if changeDetector.Enabled { + // We do not yet support the change detector for tests running across multiple nodes. + s.T.SkipNow() + return + } + + p2pOpts := a.P2POptions() + s.CurrentSetupNodeID = len(s.Nodes) + + // Versioned nodes run in a separate process from a release binary that configures + // itself, so in-process options do not apply to them. + var opts *options.NodeOptionsBuilder + if a.Version == "" { + privateKey, err := crypto.GenerateEd25519() + require.NoError(s.T, err) + WithPrivateKey(&p2pOpts, privateKey) + + opts = DefaultNodeOpts() + opts.DB(). + SetRetryIntervals([]time.Duration{time.Millisecond * 1}). + SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) + opts.P2P().SetAll(p2pOpts) + } + + node, err := SetupNode(s, acpIdentity.None, a.SetupConfig, opts, a.Version) + require.NoError(s.T, err) + if node == nil { + // SetupNode already skipped the test (no release asset for this platform). + return + } + + node.P2POpts = p2pOpts + node.Version = a.Version + s.Nodes = append(s.Nodes, node) +} diff --git a/tests/action/node_config_test.go b/tests/action/node_config_test.go new file mode 100644 index 0000000000..406e5e9635 --- /dev/null +++ b/tests/action/node_config_test.go @@ -0,0 +1,144 @@ +// 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" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + m "github.com/sourcenetwork/testo/multiplier" + + "github.com/sourcenetwork/defradb/client/options" +) + +func TestNodeConfig_ImplementsAction(t *testing.T) { + // Node config must be an action so that it survives the action.Action filter + // the harness applies before handing the set to a multiplier. Anything that + // is not an action is silently dropped there, which would leave a multiplier + // targeting node config running green while changing nothing. + var _ Action = (*NodeConfig)(nil) + var _ Stateful = (*NodeConfig)(nil) + + var cfg any = &NodeConfig{} + _, ok := cfg.(Action) + assert.True(t, ok, "NodeConfig must implement Action") +} + +func TestNodeConfig_SurvivesActionFilter(t *testing.T) { + // Mirrors the filter in the integration harness' applyMultipliers: only + // elements implementing action.Action reach the multiplier engine. + testCaseActions := []any{ + RandomNetworkingConfig(), + &AddCollection{SDL: "type User { name: String }"}, + } + + var actions Actions + for _, a := range testCaseActions { + if act, ok := a.(Action); ok { + actions = append(actions, act) + } + } + + require.Len(t, actions, 2, "node config must not be dropped by the action filter") + + var found bool + for _, a := range actions { + if _, ok := a.(*NodeConfig); ok { + found = true + } + } + assert.True(t, found, "node config must be visible to multipliers") +} + +func TestNodeConfig_ApplyCanRewriteVersion(t *testing.T) { + // The reason node config became an action: a multiplier must be able to + // rewrite it as plain data. + source := Actions{ + RandomNetworkingConfig(), + RandomNetworkingConfig(), + } + + for _, a := range source { + if cfg, ok := a.(*NodeConfig); ok { + cfg.Version = "v1.0.0" + break + } + } + + first, ok := source[0].(*NodeConfig) + require.True(t, ok) + second, ok := source[1].(*NodeConfig) + require.True(t, ok) + + assert.Equal(t, "v1.0.0", first.Version) + assert.Equal(t, "", second.Version, "only the targeted node should be rewritten") +} + +func TestNodeConfigP2POptions_WithNilNetwork_ReturnsDefaults(t *testing.T) { + cfg := &NodeConfig{} + + assert.Equal(t, options.NodeP2POptions{}, cfg.P2POptions()) +} + +func TestNodeConfigP2POptions_WithNetwork_ReturnsConfigured(t *testing.T) { + cfg := &NodeConfig{ + Network: func() options.NodeP2POptions { + return options.NodeP2POptions{EnablePubSub: true} + }, + } + + assert.True(t, cfg.P2POptions().EnablePubSub) +} + +func TestNodeConfigWithVersion_SetsVersion(t *testing.T) { + cfg := RandomNetworkingConfig().WithVersion("v1.0.0") + + assert.Equal(t, "v1.0.0", cfg.Version) + assert.NotNil(t, cfg.Network, "networking config must be preserved") +} + +func TestNodeConfigWithVersion_DoesNotMutateReceiver(t *testing.T) { + // WithVersion takes a pointer receiver, so it must copy rather than write + // through to the shared value a constructor handed out. + original := RandomNetworkingConfig() + + versioned := original.WithVersion("v1.0.0") + + assert.Equal(t, "", original.Version, "WithVersion must not mutate its receiver") + assert.Equal(t, "v1.0.0", versioned.Version) + assert.NotSame(t, original, versioned) +} + +func TestRandomNetworkingConfig_ReturnsPointer(t *testing.T) { + // The pointer return is what lets the value satisfy Action while leaving the + // existing call sites, which only ever append the result, untouched. + cfg := RandomNetworkingConfig() + + require.NotNil(t, cfg) + var _ Action = cfg +} + +func TestNodeConfig_ZeroValueIsNative(t *testing.T) { + cfg := &NodeConfig{} + + assert.Equal(t, "", cfg.Version, "the zero value must describe a native, current-build node") +} + +func TestNodeConfig_NotAnActionAwareSkipper(t *testing.T) { + // Node config carries no skip logic of its own; skipping is decided by the + // multipliers that rewrite it. + var cfg any = &NodeConfig{} + _, ok := cfg.(m.ActionAwareSkipper) + assert.False(t, ok) +} diff --git a/tests/action/node_options.go b/tests/action/node_options.go new file mode 100644 index 0000000000..9dde8a125f --- /dev/null +++ b/tests/action/node_options.go @@ -0,0 +1,169 @@ +// 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" + "strconv" + "sync" + "time" + + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/client/options" + changeDetector "github.com/sourcenetwork/defradb/tests/change_detector" + "github.com/sourcenetwork/defradb/tests/state" +) + +const ( + memoryBadgerEnvName = "DEFRA_BADGER_MEMORY" + fileBadgerEnvName = "DEFRA_BADGER_FILE" + badgerEncryptionEnvName = "DEFRA_BADGER_ENCRYPTION" + levelEnvName = "DEFRA_LEVEL" + inMemoryEnvName = "DEFRA_IN_MEMORY" + lensTypeEnvName = "DEFRA_LENS_TYPE" + + lensPoolSize = 2 +) + +const ( + BadgerIMType state.DatabaseType = "badger-in-memory" + DefraIMType state.DatabaseType = "defra-memory-datastore" + BadgerFileType state.DatabaseType = "badger-file-system" + LevelStoreType state.DatabaseType = "level" +) + +const ( + // NoneKMSType is the none KMS type. It is used to indicate that no KMS should be used. + NoneKMSType state.KMSType = "none" + // PubSubKMSType is the PubSub KMS type. + PubSubKMSType state.KMSType = "pubsub" +) + +// NodeSetupConfig carries the test-level settings node setup needs. +// +// Node setup reads only these few values, so taking them directly keeps it +// independent of the test case type. +type NodeSetupConfig struct { + // EnableSigning enables document signing on the node. + EnableSigning bool + // HTTP overrides the node's HTTP server settings when set. + HTTP immutable.Option[options.NodeHTTPOptions] + // IsDocumentACPTest reports whether the test uses document ACP, which + // decides whether a SourceHub instance is needed at all. + IsDocumentACPTest bool +} + +func applyHTTPOptions(opts *options.NodeOptionsBuilder, httpOpts options.NodeHTTPOptions) { + httpBuilder := opts.HTTP() + if httpOpts.Address != "" { + httpBuilder.SetAddress(httpOpts.Address) + } + if len(httpOpts.AllowedOrigins) > 0 { + httpBuilder.SetAllowedOrigins(httpOpts.AllowedOrigins...) + } + if httpOpts.TLSCertPath != "" { + httpBuilder.SetCertPath(httpOpts.TLSCertPath) + } + if httpOpts.TLSKeyPath != "" { + httpBuilder.SetKeyPath(httpOpts.TLSKeyPath) + } + if httpOpts.ReadTimeout != 0 { + httpBuilder.SetReadTimeout(httpOpts.ReadTimeout) + } + if httpOpts.WriteTimeout != 0 { + httpBuilder.SetWriteTimeout(httpOpts.WriteTimeout) + } + if httpOpts.IdleTimeout != 0 { + httpBuilder.SetIdleTimeout(httpOpts.IdleTimeout) + } + if httpOpts.TxnTTL != 0 { + httpBuilder.SetTxnTTL(httpOpts.TxnTTL) + } + if httpOpts.TxnTTLTick != 0 { + httpBuilder.SetTxnTTLTick(httpOpts.TxnTTLTick) + } + if httpOpts.TxnTTLBuckets != 0 { + httpBuilder.SetTxnTTLBuckets(httpOpts.TxnTTLBuckets) + } +} + +var ( + // BadgerInMemory, BadgerFile, InMemoryStore and LevelStore select the store + // types under test. Node setup and the test harness both read them, so they + // are resolved once here rather than copied into each package. + BadgerInMemory bool + BadgerFile bool + InMemoryStore bool + LevelStore bool + + // DatabaseDir is the path a restarting node reopens its store from. It is + // set by the harness around restart actions. + DatabaseDir string + + // LensType is the lens runtime under test. + LensType options.NodeLensRuntimeType + + // BadgerEncryption reports whether the badger store is encrypted. + BadgerEncryption bool + + encryptionKey []byte + // encryptionKeyOnce guards the lazy, process-wide initialization of + // encryptionKey so concurrent node setups don't race on it. + encryptionKeyOnce sync.Once + encryptionKeyErr error +) + +func init() { + // We use environment variables instead of flags `go test ./...` throws for all packages + // that don't have the flag defined + BadgerFile, _ = strconv.ParseBool(os.Getenv(fileBadgerEnvName)) + BadgerInMemory, _ = strconv.ParseBool(os.Getenv(memoryBadgerEnvName)) + InMemoryStore, _ = strconv.ParseBool(os.Getenv(inMemoryEnvName)) + LevelStore, _ = strconv.ParseBool(os.Getenv((levelEnvName))) + BadgerEncryption, _ = strconv.ParseBool(os.Getenv(badgerEncryptionEnvName)) + LensType = options.NodeLensRuntimeType(os.Getenv(lensTypeEnvName)) + + if changeDetector.Enabled { + // Change detector only uses badger file db type. + BadgerFile = true + BadgerInMemory = false + InMemoryStore = false + LevelStore = false + } else if !BadgerInMemory && !BadgerFile && !InMemoryStore && !LevelStore { + // Default is to test all but filesystem db types. + BadgerFile = false + BadgerInMemory = true + InMemoryStore = false + LevelStore = false + } +} + +// DefaultNodeOpts returns the node options shared by every test node. +func DefaultNodeOpts() *options.NodeOptionsBuilder { + opt := options.Node(). + // The test framework sets this up elsewhere when required so that it may be wrapped + // into a [client.TxnStore]. + SetDisableAPI(true). + // The p2p is configured in the tests by [NodeConfig] actions, we disable it here + // to keep the tests as lightweight as possible. + SetDisableP2P(true) + + opt.DB(). + SetLensPoolSize(lensPoolSize). + SetLensRuntime(LensType). + // The default is 5 and that is never going to be needed in a testing scenario where all the + // nodes are on the same machine with no network latency. + SetP2PBlockSyncTimeout(1 * time.Second) + + return opt +} diff --git a/tests/integration/db_setup.go b/tests/action/node_setup.go similarity index 95% rename from tests/integration/db_setup.go rename to tests/action/node_setup.go index 9e27830591..1a1c966c47 100644 --- a/tests/integration/db_setup.go +++ b/tests/action/node_setup.go @@ -11,7 +11,7 @@ //go:build !js -package tests +package action import ( "context" @@ -37,7 +37,7 @@ import ( ) func createBadgerEncryptionKey() error { - if !badgerEncryption { + if !BadgerEncryption { return nil } encryptionKeyOnce.Do(func() { @@ -57,10 +57,10 @@ func createBadgerEncryptionKey() error { // Note: If the signature of this function is updated, don't forget to // also update the function in [tests/integration/db_setup_js.go] otherwise // the js client build may fail (the failure might not be obvious to find). -func setupNode( +func SetupNode( s *state.State, identity immutable.Option[acpIdentity.Identity], - testCase TestCase, + cfg NodeSetupConfig, opts *options.NodeOptionsBuilder, ver string, ) (*state.NodeState, error) { @@ -69,11 +69,11 @@ func setupNode( } if opts == nil { - opts = defaultNodeOpts() + opts = DefaultNodeOpts() } - opts.DB().SetEnableSigning(testCase.EnableSigning) - if testCase.HTTP.HasValue() { - applyHTTPOptions(opts, testCase.HTTP.Value()) + opts.DB().SetEnableSigning(cfg.EnableSigning) + if cfg.HTTP.HasValue() { + applyHTTPOptions(opts, cfg.HTTP.Value()) } if s.EnableSearchableEncryption { @@ -88,7 +88,7 @@ func setupNode( if err != nil { return nil, err } - if badgerEncryption && encryptionKey != nil { + if BadgerEncryption && encryptionKey != nil { opts.Store().SetBadgerEncryptionKey(encryptionKey) } @@ -98,7 +98,7 @@ func setupNode( case state.SourceHubDocumentACPType: if s.DocumentACPOptions == nil { - s.DocumentACPOptions, err = setupSourceHub(s, testCase) + s.DocumentACPOptions, err = setupSourceHub(s, cfg.IsDocumentACPTest) require.NoError(s.T, err) } opts.DocumentACP().SetAll(*s.DocumentACPOptions) @@ -109,9 +109,9 @@ func setupNode( var path string if s.DbType == BadgerFileType || s.DbType == LevelStoreType { - if databaseDir != "" { + if DatabaseDir != "" { // restarting database - path = databaseDir + path = DatabaseDir } else if changeDetector.Enabled { // change detector path = changeDetector.DatabaseDir(s.T) diff --git a/tests/integration/db_setup_js.go b/tests/action/node_setup_js.go similarity index 88% rename from tests/integration/db_setup_js.go rename to tests/action/node_setup_js.go index e918a80ce4..193d17f126 100644 --- a/tests/integration/db_setup_js.go +++ b/tests/action/node_setup_js.go @@ -9,7 +9,7 @@ // // See tests/LICENSE for details. -package tests +package action import ( "github.com/stretchr/testify/require" @@ -28,21 +28,21 @@ import ( // select the datastore implementation to use. // // ver is unused: external (cross-version) nodes are not supported under js. -func setupNode( +func SetupNode( s *state.State, identity immutable.Option[acpIdentity.Identity], - testCase TestCase, + cfg NodeSetupConfig, opts *options.NodeOptionsBuilder, ver string, ) (*state.NodeState, error) { if opts == nil { - opts = defaultNodeOpts() + opts = DefaultNodeOpts() } opts.DB(). - SetEnableSigning(testCase.EnableSigning). + SetEnableSigning(cfg.EnableSigning). SetLensRuntime(options.NodeJSLensRuntime) - if testCase.HTTP.HasValue() { - applyHTTPOptions(opts, testCase.HTTP.Value()) + if cfg.HTTP.HasValue() { + applyHTTPOptions(opts, cfg.HTTP.Value()) } // Note: Since we are hard-coding to run with badger in-mem only, we have a function that // handles some edge-cases by skipping js client testing when a db type is something else. @@ -50,14 +50,14 @@ func setupNode( // [skipJSClientIfUnsupportedDBType] opts.Store().SetBadgerInMemory(true) - switch documentACPType { + switch DocumentACPType { case state.LocalDocumentACPType: opts.DocumentACP().SetType(options.NodeLocalDocumentACPType) case state.SourceHubDocumentACPType: if s.DocumentACPOptions == nil { var err error - s.DocumentACPOptions, err = setupSourceHub(s) + s.DocumentACPOptions, err = setupSourceHub(s, cfg.IsDocumentACPTest) require.NoError(s.T, err) } opts.DocumentACP(). diff --git a/tests/integration/p2p_config.go b/tests/action/p2p_config.go similarity index 86% rename from tests/integration/p2p_config.go rename to tests/action/p2p_config.go index 0c0614d0f8..1e53b54cdf 100644 --- a/tests/integration/p2p_config.go +++ b/tests/action/p2p_config.go @@ -11,7 +11,7 @@ //go:build !js -package tests +package action import ( "net" @@ -19,8 +19,8 @@ import ( "github.com/sourcenetwork/defradb/client/options" ) -func RandomNetworkingConfig() NodeConfig { - return NodeConfig{ +func RandomNetworkingConfig() *NodeConfig { + return &NodeConfig{ Network: func() options.NodeP2POptions { return options.NodeP2POptions{ ListenAddresses: []string{"/ip4/" + getIPString() + "/tcp/0"}, @@ -55,10 +55,10 @@ func getIPString() string { return localAddr.IP.String() } -func withPrivateKey(p2pOpts *options.NodeP2POptions, key []byte) { +func WithPrivateKey(p2pOpts *options.NodeP2POptions, key []byte) { p2pOpts.PrivateKey = key } -func withListenAddresses(p2pOpts *options.NodeP2POptions, addresses ...string) { +func WithListenAddresses(p2pOpts *options.NodeP2POptions, addresses ...string) { p2pOpts.ListenAddresses = addresses } diff --git a/tests/integration/p2p_config_js.go b/tests/action/p2p_config_js.go similarity index 71% rename from tests/integration/p2p_config_js.go rename to tests/action/p2p_config_js.go index 2ddfa9786c..cc317a791c 100644 --- a/tests/integration/p2p_config_js.go +++ b/tests/action/p2p_config_js.go @@ -9,24 +9,24 @@ // // See tests/LICENSE for details. -package tests +package action import ( "github.com/sourcenetwork/defradb/client/options" ) -func RandomNetworkingConfig() NodeConfig { - return NodeConfig{ +func RandomNetworkingConfig() *NodeConfig { + return &NodeConfig{ Network: func() options.NodeP2POptions { return options.NodeP2POptions{} }, } } -func withPrivateKey(_ *options.NodeP2POptions, _ []byte) { +func WithPrivateKey(_ *options.NodeP2POptions, _ []byte) { // JS builds don't support P2P } -func withListenAddresses(_ *options.NodeP2POptions, _ ...string) { +func WithListenAddresses(_ *options.NodeP2POptions, _ ...string) { // JS builds don't support P2P } diff --git a/tests/integration/acp_dac.go b/tests/integration/acp_dac.go index 3aa236dab5..b6f1855c2e 100644 --- a/tests/integration/acp_dac.go +++ b/tests/integration/acp_dac.go @@ -12,7 +12,6 @@ package tests import ( - "os" "slices" "github.com/stretchr/testify/require" @@ -20,41 +19,21 @@ import ( "github.com/sourcenetwork/immutable" "github.com/sourcenetwork/defradb/client/options" + "github.com/sourcenetwork/defradb/tests/action" "github.com/sourcenetwork/defradb/tests/state" ) -const ( - documentACPTypeEnvName = "DEFRA_DOCUMENT_ACP_TYPE" - sourcehubImageEnvName = "DEFRA_SOURCEHUB_IMAGE" -) - -var ( - documentACPType state.DocumentACPType - sourcehubImage string -) - const ( // NoneKMSType is the none KMS type. It is used to indicate that no KMS should be used. - NoneKMSType state.KMSType = "none" + NoneKMSType = action.NoneKMSType // PubSubKMSType is the PubSub KMS type. - PubSubKMSType state.KMSType = "pubsub" + PubSubKMSType = action.PubSubKMSType ) func getKMSTypes() []state.KMSType { return []state.KMSType{PubSubKMSType} } -func init() { - documentACPType = state.DocumentACPType(os.Getenv(documentACPTypeEnvName)) - if documentACPType == "" { - documentACPType = state.LocalDocumentACPType - } - sourcehubImage = os.Getenv(sourcehubImageEnvName) - if sourcehubImage == "" { - sourcehubImage = "ghcr.io/sourcenetwork/sourcehub:dev" - } -} - // AddDACPolicy will attempt to add the given policy using DefraDB's Document ACP system. type AddDACPolicy struct { // NodeID may hold the ID (index) of the node we want to add policy to. diff --git a/tests/integration/client_setup.go b/tests/integration/client_setup.go index 8ba072ec97..2bceb34d98 100644 --- a/tests/integration/client_setup.go +++ b/tests/integration/client_setup.go @@ -13,18 +13,6 @@ package tests -import ( - "context" - "fmt" - - cbindings "github.com/sourcenetwork/defradb/cbindings" - "github.com/sourcenetwork/defradb/node" - "github.com/sourcenetwork/defradb/tests/clients" - "github.com/sourcenetwork/defradb/tests/clients/cli" - "github.com/sourcenetwork/defradb/tests/clients/http" - "github.com/sourcenetwork/defradb/tests/state" -) - func init() { if !goClient && !httpClient && !cliClient && !cClient { // Default is to test go client type. @@ -35,41 +23,3 @@ func init() { skipBackupTests = true } } - -// setupClient returns the client implementation for the current -// testing state. The client type on the test state is used to -// select the client implementation to use. -func setupClient(s *state.State, nodeObj *node.Node) (clients.Client, error) { - switch s.ClientType { - case state.HTTPClientType: - return http.NewWrapper(nodeObj) - - case state.CLIClientType: - return cli.NewWrapper(nodeObj, s.SourcehubAddress) - - case state.GoClientType: - return newGoClientWrapper(nodeObj), nil - - case state.CClientType: - return cbindings.NewCWrapper(nodeObj) - - default: - return nil, fmt.Errorf("invalid client type: %v", s.ClientType) - } -} - -type goClientWrapper struct { - node.DB - node *node.Node -} - -func newGoClientWrapper(n *node.Node) *goClientWrapper { - return &goClientWrapper{ - DB: n.DB, - node: n, - } -} - -func (w *goClientWrapper) Close() { - _ = w.node.Close(context.Background()) -} diff --git a/tests/integration/client_setup_js.go b/tests/integration/client_setup_js.go index 95642fd3ef..7357e32223 100644 --- a/tests/integration/client_setup_js.go +++ b/tests/integration/client_setup_js.go @@ -11,13 +11,6 @@ package tests -import ( - "github.com/sourcenetwork/defradb/node" - "github.com/sourcenetwork/defradb/tests/clients" - "github.com/sourcenetwork/defradb/tests/clients/js" - "github.com/sourcenetwork/defradb/tests/state" -) - func init() { goClient = false httpClient = false @@ -29,10 +22,3 @@ func init() { // Backup API is not suitable for browser environments skipBackupTests = true } - -// setupClient returns the client implementation for the current -// testing state. The client type on the test state is used to -// select the client implementation to use. -func setupClient(_ *state.State, node *node.Node) (impl clients.Client, err error) { - return js.NewWrapper(node) -} diff --git a/tests/integration/db.go b/tests/integration/db.go index d77dd8c000..385cb68e37 100644 --- a/tests/integration/db.go +++ b/tests/integration/db.go @@ -13,91 +13,23 @@ package tests import ( "context" - "os" - "strconv" - "sync" "testing" - "time" "github.com/sourcenetwork/defradb/client/options" "github.com/sourcenetwork/defradb/node" - changeDetector "github.com/sourcenetwork/defradb/tests/change_detector" - "github.com/sourcenetwork/defradb/tests/state" + "github.com/sourcenetwork/defradb/tests/action" ) +// Store type selection and the shared node options live in the action package +// alongside node setup. They are aliased here so the harness and existing tests +// keep reading a single source of truth rather than a second copy. const ( - memoryBadgerEnvName = "DEFRA_BADGER_MEMORY" - fileBadgerEnvName = "DEFRA_BADGER_FILE" - fileBadgerPathEnvName = "DEFRA_BADGER_FILE_PATH" - badgerEncryptionEnvName = "DEFRA_BADGER_ENCRYPTION" - levelEnvName = "DEFRA_LEVEL" - inMemoryEnvName = "DEFRA_IN_MEMORY" + BadgerIMType = action.BadgerIMType + DefraIMType = action.DefraIMType + BadgerFileType = action.BadgerFileType + LevelStoreType = action.LevelStoreType ) -const ( - BadgerIMType state.DatabaseType = "badger-in-memory" - DefraIMType state.DatabaseType = "defra-memory-datastore" - BadgerFileType state.DatabaseType = "badger-file-system" - LevelStoreType state.DatabaseType = "level" -) - -var ( - badgerInMemory bool - badgerFile bool - inMemoryStore bool - levelStore bool - databaseDir string - badgerEncryption bool - encryptionKey []byte - // encryptionKeyOnce guards the lazy, process-wide initialization of - // encryptionKey so concurrent node setups don't race on it. - encryptionKeyOnce sync.Once - encryptionKeyErr error -) - -func init() { - // We use environment variables instead of flags `go test ./...` throws for all packages - // that don't have the flag defined - badgerFile, _ = strconv.ParseBool(os.Getenv(fileBadgerEnvName)) - badgerInMemory, _ = strconv.ParseBool(os.Getenv(memoryBadgerEnvName)) - inMemoryStore, _ = strconv.ParseBool(os.Getenv(inMemoryEnvName)) - levelStore, _ = strconv.ParseBool(os.Getenv((levelEnvName))) - badgerEncryption, _ = strconv.ParseBool(os.Getenv(badgerEncryptionEnvName)) - - if changeDetector.Enabled { - // Change detector only uses badger file db type. - badgerFile = true - badgerInMemory = false - inMemoryStore = false - levelStore = false - } else if !badgerInMemory && !badgerFile && !inMemoryStore && !levelStore { - // Default is to test all but filesystem db types. - badgerFile = false - badgerInMemory = true - inMemoryStore = false - levelStore = false - } -} - -func defaultNodeOpts() *options.NodeOptionsBuilder { - opt := options.Node(). - // The test framework sets this up elsewhere when required so that it may be wrapped - // into a [client.TxnStore]. - SetDisableAPI(true). - // The p2p is configured in the tests by [NodeConfig] actions, we disable it here - // to keep the tests as lightweight as possible. - SetDisableP2P(true) - - opt.DB(). - SetLensPoolSize(lensPoolSize). - SetLensRuntime(lensType). - // The default is 5 and that is never going to be needed in a testing scenario where all the - // nodes are on the same machine with no network latency. - SetP2PBlockSyncTimeout(1 * time.Second) - - return opt -} - func NewBadgerMemoryDB(ctx context.Context) (node.DB, error) { opts := options.Node(). SetDisableP2P(true). diff --git a/tests/integration/lens.go b/tests/integration/lens.go index 0d774f1429..04a0fc88db 100644 --- a/tests/integration/lens.go +++ b/tests/integration/lens.go @@ -12,8 +12,6 @@ package tests import ( - "os" - "github.com/stretchr/testify/require" "github.com/sourcenetwork/defradb/client" @@ -24,18 +22,6 @@ import ( "github.com/sourcenetwork/immutable" ) -const ( - lensTypeEnvName = "DEFRA_LENS_TYPE" -) - -var ( - lensType options.NodeLensRuntimeType -) - -func init() { - lensType = options.NodeLensRuntimeType(os.Getenv(lensTypeEnvName)) -} - // ConfigureMigration is a test action which will configure a Lens migration using the // provided configuration. type ConfigureMigration struct { diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index e277d02606..565ee67938 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -121,80 +121,34 @@ type KMS struct { // the first item that is neither an AddCollection, AddDoc or UpdateDoc action. type SetupComplete struct{} -// ConfigureNode returns the P2P options for a new Defra node. -type ConfigureNode func() options.NodeP2POptions - -// NodeConfig allows the explicit configuration of new Defra nodes. The zero value -// is a native, current-build node with default networking. -// -// If no nodes are explicitly configured, a default one will be setup. There is no -// upper limit to the number that can be configured. -// -// Nodes may be explicitly referenced by index by other actions using `NodeID` properties. -// If the action has a `NodeID` property and it is not specified, the action will be -// effected on all nodes. -// -// Configuration is held as plain data wherever possible so a future multiplier can -// rewrite it to run existing tests under other node configurations. -// -// This lives here rather than in the tests/action package because node creation is -// entangled with this package's setup path, which tests/action cannot import. -type NodeConfig struct { - // Version, when set (e.g. "v1.0.0"), runs the node as an external process from - // that published release binary instead of natively in-process. - Version string - // Network returns the node's P2P options. Nil means default networking. - Network ConfigureNode -} - -// P2POptions returns the configured P2P options, or the defaults if no networking -// config was supplied. -func (cfg NodeConfig) P2POptions() options.NodeP2POptions { - if cfg.Network == nil { - return options.NodeP2POptions{} +// RandomNetworkingConfig returns a node configured with random networking. +var RandomNetworkingConfig = action.RandomNetworkingConfig + +// nodeSetupConfig returns the node setup settings for this test case. +func (tc TestCase) nodeSetupConfig() action.NodeSetupConfig { + return action.NodeSetupConfig{ + EnableSigning: tc.EnableSigning, + HTTP: tc.HTTP, + IsDocumentACPTest: hasDocumentACPActions(tc.Actions), } - return cfg.Network() } -// WithVersion returns a copy of the config that runs the node as an external process -// from the given published release, e.g. "v1.0.0". -func (cfg NodeConfig) WithVersion(version string) NodeConfig { - cfg.Version = version - return cfg -} - -func applyHTTPOptions(opts *options.NodeOptionsBuilder, httpOpts options.NodeHTTPOptions) { - httpBuilder := opts.HTTP() - if httpOpts.Address != "" { - httpBuilder.SetAddress(httpOpts.Address) - } - if len(httpOpts.AllowedOrigins) > 0 { - httpBuilder.SetAllowedOrigins(httpOpts.AllowedOrigins...) - } - if httpOpts.TLSCertPath != "" { - httpBuilder.SetCertPath(httpOpts.TLSCertPath) - } - if httpOpts.TLSKeyPath != "" { - httpBuilder.SetKeyPath(httpOpts.TLSKeyPath) - } - if httpOpts.ReadTimeout != 0 { - httpBuilder.SetReadTimeout(httpOpts.ReadTimeout) - } - if httpOpts.WriteTimeout != 0 { - httpBuilder.SetWriteTimeout(httpOpts.WriteTimeout) - } - if httpOpts.IdleTimeout != 0 { - httpBuilder.SetIdleTimeout(httpOpts.IdleTimeout) - } - if httpOpts.TxnTTL != 0 { - httpBuilder.SetTxnTTL(httpOpts.TxnTTL) - } - if httpOpts.TxnTTLTick != 0 { - httpBuilder.SetTxnTTLTick(httpOpts.TxnTTLTick) - } - if httpOpts.TxnTTLBuckets != 0 { - httpBuilder.SetTxnTTLBuckets(httpOpts.TxnTTLBuckets) +// hasDocumentACPActions reports whether the action set uses document ACP. +// +// Spinning up a SourceHub instance is slow, so tests that do not need one are +// skipped when SourceHub ACP is selected. +func hasDocumentACPActions(actions []any) bool { + for _, a := range actions { + switch a.(type) { + case + AddDACPolicy, + AddDACActorRelationship, + *action.AddDACCollectionActorRelationship, + DeleteDACActorRelationship: + return true + } } + return false } // Restart is an action that will close and then start all nodes. diff --git a/tests/integration/utils.go b/tests/integration/utils.go index a98da355eb..e4f5783d60 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -39,7 +39,6 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/client/options" "github.com/sourcenetwork/defradb/client/request" - "github.com/sourcenetwork/defradb/crypto" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/internal/db" "github.com/sourcenetwork/defradb/tests/action" @@ -85,9 +84,6 @@ var ( const ( // subscriptionTimeout is the maximum time to wait for subscription results to be returned. subscriptionTimeout = 1 * time.Second - // Instantiating lenses is expensive, and our tests do not benefit from a large number of them, - // so we explicitly set it to a low value. - lensPoolSize = 2 ) const testJSONFile = "/test.json" @@ -178,16 +174,16 @@ func ExecuteTestCase( } var databases []state.DatabaseType - if badgerInMemory { + if action.BadgerInMemory { databases = append(databases, BadgerIMType) } - if badgerFile { + if action.BadgerFile { databases = append(databases, BadgerFileType) } - if inMemoryStore { + if action.InMemoryStore { databases = append(databases, DefraIMType) } - if levelStore { + if action.LevelStore { databases = append(databases, LevelStoreType) } @@ -226,7 +222,7 @@ func ExecuteTestCase( kms, dbt, ct, - documentACPType, + action.DocumentACPType, ) } @@ -260,8 +256,8 @@ func executeTestCase( corelog.Any("database", dbt), corelog.Any("client", clientType), corelog.Any("mutationType", state.ActiveMutationType), - corelog.String("databaseDir", databaseDir), - corelog.Bool("badgerEncryption", badgerEncryption), + corelog.String("databaseDir", action.DatabaseDir), + corelog.Bool("badgerEncryption", action.BadgerEncryption), corelog.Bool("skipNetworkTests", skipNetworkTests), corelog.Bool("changeDetector.Enabled", changeDetector.Enabled), corelog.Bool("changeDetector.SetupOnly", changeDetector.SetupOnly), @@ -360,11 +356,9 @@ func performAction( switch action := act.(type) { case action.Action: + // [action.NodeConfig] is an action, so node setup runs from here too. action.Execute() - case NodeConfig: - configureNode(s, testCase, action) - case Restart: restartNodes(s, testCase) @@ -835,7 +829,7 @@ func createsDocsOnMultipleNodes(testCase *TestCase) bool { nodeCount := 0 for _, a := range testCase.Actions { switch a.(type) { - case NodeConfig: + case *action.NodeConfig: nodeCount++ } } @@ -994,15 +988,19 @@ func actionTransactionID(a any) (int, bool) { // setStartingNodes adds a set of initial Defra nodes for the test to execute against. // -// If a node(s) has been explicitly configured via a `NodeConfig` action then no new +// If a node(s) has been explicitly configured via a [action.NodeConfig] action then no new // nodes will be added. func setStartingNodes( s *state.State, testCase TestCase, ) { - for _, action := range testCase.Actions { - switch action.(type) { - case NodeConfig: + setupConfig := testCase.nodeSetupConfig() + for _, a := range testCase.Actions { + switch cfg := a.(type) { + case *action.NodeConfig: + // Node setup needs a few test-level settings that the action cannot + // reach on its own. + cfg.SetupConfig = setupConfig s.IsNetworkEnabled = true } } @@ -1010,12 +1008,12 @@ func setStartingNodes( // If nodes have not been explicitly configured via actions, setup a default one. if !s.IsNetworkEnabled { s.CurrentSetupNodeID = 0 - nodeBuilder := defaultNodeOpts() + nodeBuilder := action.DefaultNodeOpts() nodeBuilder.DB().SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) - st, err := setupNode( + st, err := action.SetupNode( s, acpIdentity.None, - testCase, + testCase.nodeSetupConfig(), nodeBuilder, "", ) @@ -1025,40 +1023,40 @@ func setStartingNodes( } } -func startNodes(s *state.State, testCase TestCase, action Start) { - nodeIDs, nodes := getNodesWithIDs(action.NodeID, s.Nodes) +func startNodes(s *state.State, testCase TestCase, start Start) { + nodeIDs, nodes := getNodesWithIDs(start.NodeID, s.Nodes) // We need to restart the nodes in reverse order, to avoid dial backoff issues. for index := len(nodes) - 1; index >= 0; index-- { nodeID := nodeIDs[index] - originalPath := databaseDir - databaseDir = s.Nodes[nodeID].DbPath + originalPath := action.DatabaseDir + action.DatabaseDir = s.Nodes[nodeID].DbPath s.CurrentSetupNodeID = nodeID p2pOpts := s.Nodes[nodeID].P2POpts - withListenAddresses(&p2pOpts, s.Nodes[nodeID].CachedAddresses...) - opts := defaultNodeOpts() + action.WithListenAddresses(&p2pOpts, s.Nodes[nodeID].CachedAddresses...) + opts := action.DefaultNodeOpts() opts.DB().SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) opts.P2P().SetAll(p2pOpts) - opts.NodeACP().SetEnabled(action.EnableNAC) - node, err := setupNode( + opts.NodeACP().SetEnabled(start.EnableNAC) + node, err := action.SetupNode( s, - getIdentityOption(s, action.Identity), - testCase, + getIdentityOption(s, start.Identity), + testCase.nodeSetupConfig(), opts, s.Nodes[nodeID].Version, ) - databaseDir = originalPath + action.DatabaseDir = originalPath - expectedErrorRaised := AssertError(s.T, err, action.ExpectedError) - assertExpectedErrorRaised(s.T, action.ExpectedError, expectedErrorRaised) + expectedErrorRaised := AssertError(s.T, err, start.ExpectedError) + assertExpectedErrorRaised(s.T, start.ExpectedError, expectedErrorRaised) if expectedErrorRaised { // If we are testing for failure on start of a node, there will be panics if we don't return // when there are errors, so we exit here to assert errors on start. return } - require.Equal(s.T, action.ExpectedError, "") + require.Equal(s.T, start.ExpectedError, "") node.P2P = s.Nodes[nodeID].P2P s.Nodes[nodeID] = node } @@ -1248,50 +1246,6 @@ func refreshCollections( } } -// configureNode configures and starts a new Defra node using the provided configuration. -// -// Any errors generated during configuration will result in a test failure. -func configureNode( - s *state.State, - testCase TestCase, - cfg NodeConfig, -) { - if changeDetector.Enabled { - // We do not yet support the change detector for tests running across multiple nodes. - s.T.SkipNow() - return - } - - p2pOpts := cfg.P2POptions() - s.CurrentSetupNodeID = len(s.Nodes) - - // Versioned nodes run in a separate process from a release binary that configures - // itself, so in-process options do not apply to them. - var opts *options.NodeOptionsBuilder - if cfg.Version == "" { - privateKey, err := crypto.GenerateEd25519() - require.NoError(s.T, err) - withPrivateKey(&p2pOpts, privateKey) - - opts = defaultNodeOpts() - opts.DB(). - SetRetryIntervals([]time.Duration{time.Millisecond * 1}). - SetNodeIdentity(state.GetIdentity(s, NodeIdentity(s.CurrentSetupNodeID))) - opts.P2P().SetAll(p2pOpts) - } - - node, err := setupNode(s, acpIdentity.None, testCase, opts, cfg.Version) - require.NoError(s.T, err) - if node == nil { - // setupNode already skipped the test (no release asset for this platform). - return - } - - node.P2POpts = p2pOpts - node.Version = cfg.Version - s.Nodes = append(s.Nodes, node) -} - func refreshDocuments( s *state.State, testCase TestCase, @@ -2339,14 +2293,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) } } } @@ -2382,7 +2336,7 @@ func skipIfNetworkTest(t testing.TB, actions []any) { hasNetworkAction := false for _, act := range actions { switch act.(type) { - case NodeConfig: + case *action.NodeConfig: hasNetworkAction = true } } From 569033722d2f0465ee5782e873f46d0c4cd34867 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 5 Aug 2026 00:05:36 +0200 Subject: [PATCH 04/24] Polish --- tests/action/node_options.go | 8 ++++---- tests/action/node_setup.go | 4 ++-- tests/action/node_setup_js.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/action/node_options.go b/tests/action/node_options.go index 9dde8a125f..49ce3ecca2 100644 --- a/tests/action/node_options.go +++ b/tests/action/node_options.go @@ -110,8 +110,8 @@ var ( // set by the harness around restart actions. DatabaseDir string - // LensType is the lens runtime under test. - LensType options.NodeLensRuntimeType + // lensType is the lens runtime under test. + lensType options.NodeLensRuntimeType // BadgerEncryption reports whether the badger store is encrypted. BadgerEncryption bool @@ -131,7 +131,7 @@ func init() { InMemoryStore, _ = strconv.ParseBool(os.Getenv(inMemoryEnvName)) LevelStore, _ = strconv.ParseBool(os.Getenv((levelEnvName))) BadgerEncryption, _ = strconv.ParseBool(os.Getenv(badgerEncryptionEnvName)) - LensType = options.NodeLensRuntimeType(os.Getenv(lensTypeEnvName)) + lensType = options.NodeLensRuntimeType(os.Getenv(lensTypeEnvName)) if changeDetector.Enabled { // Change detector only uses badger file db type. @@ -160,7 +160,7 @@ func DefaultNodeOpts() *options.NodeOptionsBuilder { opt.DB(). SetLensPoolSize(lensPoolSize). - SetLensRuntime(LensType). + SetLensRuntime(lensType). // The default is 5 and that is never going to be needed in a testing scenario where all the // nodes are on the same machine with no network latency. SetP2PBlockSyncTimeout(1 * time.Second) diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index 1a1c966c47..a01c0fc69a 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -46,7 +46,7 @@ func createBadgerEncryptionKey() error { return encryptionKeyErr } -// setupNode returns the database implementation for the current +// SetupNode returns the database implementation for the current // testing state. The database type on the test state is used to // select the datastore implementation to use. // @@ -55,7 +55,7 @@ func createBadgerEncryptionKey() error { // ignored in that case (the external process gets its own flags). // // Note: If the signature of this function is updated, don't forget to -// also update the function in [tests/integration/db_setup_js.go] otherwise +// also update the function in [tests/action/node_setup_js.go] otherwise // the js client build may fail (the failure might not be obvious to find). func SetupNode( s *state.State, diff --git a/tests/action/node_setup_js.go b/tests/action/node_setup_js.go index 193d17f126..a9c2d33c93 100644 --- a/tests/action/node_setup_js.go +++ b/tests/action/node_setup_js.go @@ -23,7 +23,7 @@ import ( "github.com/sourcenetwork/defradb/tests/state" ) -// setupNode returns the database implementation for the current +// SetupNode returns the database implementation for the current // testing state. The database type on the test state is used to // select the datastore implementation to use. // From 48781b916c54fed2a97329e971dd0983d66224d8 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 5 Aug 2026 11:19:17 +0200 Subject: [PATCH 05/24] Cross-version multiplier --- .github/workflows/test-coverage.yml | 61 +++++++ tests/action/assert_request.go | 10 +- tests/action/results.go | 11 ++ tests/integration/events.go | 84 +++++++++- tests/multiplier/cross_version.go | 142 ++++++++++++++++ tests/multiplier/cross_version_test.go | 215 +++++++++++++++++++++++++ 6 files changed, 515 insertions(+), 8 deletions(-) create mode 100644 tests/multiplier/cross_version.go create mode 100644 tests/multiplier/cross_version_test.go diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 0b8c0143ec..c2c2bd282d 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 downloaded over plain HTTP, 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/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/results.go b/tests/action/results.go index 09abf77804..4d20c57aef 100644 --- a/tests/action/results.go +++ b/tests/action/results.go @@ -145,6 +145,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/integration/events.go b/tests/integration/events.go index a1fc654d28..24ab9ed8b9 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" @@ -258,9 +259,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 +297,13 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { totalPending += len(cidSet) } + // An external node runs in another process, so its event bus cannot be + // read from here. Poll it for the same CIDs instead. + if node.IsExternal { + waitForCommitsOnNode(s, node, pending) + continue + } + for totalPending > 0 { var evt event.MergeComplete select { @@ -338,6 +343,79 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { } } +// errCommitCIDNotFound is returned when querying a CID the node does not hold. +const errCommitCIDNotFound = "cid either does not exist or belong to document" + +// waitForCommitsOnNode waits until every pending CID is present on the node. +// +// It queries the node instead of reading its event bus, so it works for a node +// running in another process. pending maps a doc ID or collection ID to the CIDs +// still expected to arrive. +func waitForCommitsOnNode(s *state.State, node *state.NodeState, pending map[string]map[cid.Cid]struct{}) { + remaining := 0 + for _, cidSet := range pending { + remaining += len(cidSet) + } + if remaining == 0 { + return + } + + deadline := time.Now().Add(30 * eventTimeout) + for { + missing := false + for key, cidSet := range pending { + for c := range cidSet { + if !hasCommit(s, node, c) { + missing = true + continue + } + delete(cidSet, c) + node.P2P.ActualDAGHeads[key] = state.DocHeadState{CID: c} + } + } + if !missing { + return + } + if time.Now().After(deadline) { + require.Fail(s.T, "timeout waiting for commits to sync to external node", + "node still missing: %v", pending) + } + time.Sleep(100 * time.Millisecond) + } +} + +// hasCommit reports whether the node holds the block for the given CID. +// +// Querying a CID the node does not have yet is an error rather than an empty +// result, so that case reports false and the caller keeps waiting. Any other +// error is a real problem and fails the test. +func hasCommit(s *state.State, node *state.NodeState, target cid.Cid) bool { + result := node.ExecRequest( + s.Ctx, + fmt.Sprintf(`query { _commits(cid: %q) { cid } }`, target.String()), + ) + for _, err := range result.GQL.Errors { + if strings.Contains(err.Error(), errCommitCIDNotFound) { + return false + } + require.NoError(s.T, err, "commit query failed on node") + } + + 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/multiplier/cross_version.go b/tests/multiplier/cross_version.go new file mode 100644 index 0000000000..98114bb676 --- /dev/null +++ b/tests/multiplier/cross_version.go @@ -0,0 +1,142 @@ +// 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 := nodeConfigs(actions) + if len(nodes) < 2 { + return true + } + + for _, node := range nodes { + if node.Version != "" { + return true + } + } + + // A test that changes the schema on one node and then asserts against it is + // checking how the two schemas behave, not how the two versions talk. Running + // it against a released binary asks a different question than it was written + // for. + return patchesCollectionOn(actions, m.versionedNodeID(len(nodes))) +} + +// versionedNodeID returns the index of the node that will carry the older version. +func (m *crossVersion) versionedNodeID(nodeCount int) int { + if m.oldNodeFirst { + return 0 + } + return nodeCount - 1 +} + +// patchesCollectionOn reports whether the actions patch a collection on the +// given node. A patch with no node set applies to every node. +func patchesCollectionOn(actions action.Actions, nodeID int) bool { + for _, a := range actions { + patch, ok := a.(*action.PatchCollection) + if !ok { + continue + } + if !patch.NodeID.HasValue() || patch.NodeID.Value() == nodeID { + return true + } + } + return false +} + +func (m *crossVersion) Apply(source action.Actions) action.Actions { + nodes := nodeConfigs(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.NodeConfig); ok && cfg == target { + result[i] = cfg.WithVersion(CrossVersionTargetVersion) + continue + } + result[i] = a + } + + return result +} + +// nodeConfigs returns the node configurations in the action set, in order. +func nodeConfigs(actions action.Actions) []*action.NodeConfig { + var configs []*action.NodeConfig + for _, a := range actions { + if cfg, ok := a.(*action.NodeConfig); ok { + configs = append(configs, cfg) + } + } + return configs +} diff --git a/tests/multiplier/cross_version_test.go b/tests/multiplier/cross_version_test.go new file mode 100644 index 0000000000..8ff8b13b27 --- /dev/null +++ b/tests/multiplier/cross_version_test.go @@ -0,0 +1,215 @@ +// 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" +) + +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, result[0].(*action.NodeConfig).Version) + assert.Equal(t, "", result[1].(*action.NodeConfig).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, "", result[0].(*action.NodeConfig).Version) + assert.Equal(t, CrossVersionTargetVersion, result[1].(*action.NodeConfig).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, "", result[0].(*action.NodeConfig).Version) + assert.Equal(t, "", result[1].(*action.NodeConfig).Version) + assert.Equal(t, CrossVersionTargetVersion, result[2].(*action.NodeConfig).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 := result[0].(*action.NodeConfig) + 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, "", result[0].(*action.NodeConfig).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_WhenPatchingTheVersionedNode_Skips(t *testing.T) { + // Node 0 is patched, and old-source is the direction that versions node 0. + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + &action.PatchCollection{NodeID: immutable.Some(0)}, + } + + assert.True(t, oldSource().ShouldSkip(actions)) + assert.False(t, newSource().ShouldSkip(actions), "the other direction leaves node 0 native") +} + +func TestCrossVersionShouldSkip_WhenPatchingAllNodes_Skips(t *testing.T) { + // A patch with no node set applies everywhere, so it always hits the + // versioned node. + actions := action.Actions{ + action.RandomNetworkingConfig(), + action.RandomNetworkingConfig(), + &action.PatchCollection{}, + } + + assert.True(t, oldSource().ShouldSkip(actions)) + assert.True(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)) +} From bd7faa7092955fb832257a74969726f3338e9f41 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 6 Aug 2026 11:24:37 +0200 Subject: [PATCH 06/24] Remove heuristic --- tests/multiplier/cross_version.go | 27 ----------------------- tests/multiplier/cross_version_test.go | 30 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/tests/multiplier/cross_version.go b/tests/multiplier/cross_version.go index 98114bb676..1c2f4a3dc7 100644 --- a/tests/multiplier/cross_version.go +++ b/tests/multiplier/cross_version.go @@ -77,33 +77,6 @@ func (m *crossVersion) ShouldSkip(actions action.Actions) bool { } } - // A test that changes the schema on one node and then asserts against it is - // checking how the two schemas behave, not how the two versions talk. Running - // it against a released binary asks a different question than it was written - // for. - return patchesCollectionOn(actions, m.versionedNodeID(len(nodes))) -} - -// versionedNodeID returns the index of the node that will carry the older version. -func (m *crossVersion) versionedNodeID(nodeCount int) int { - if m.oldNodeFirst { - return 0 - } - return nodeCount - 1 -} - -// patchesCollectionOn reports whether the actions patch a collection on the -// given node. A patch with no node set applies to every node. -func patchesCollectionOn(actions action.Actions, nodeID int) bool { - for _, a := range actions { - patch, ok := a.(*action.PatchCollection) - if !ok { - continue - } - if !patch.NodeID.HasValue() || patch.NodeID.Value() == nodeID { - return true - } - } return false } diff --git a/tests/multiplier/cross_version_test.go b/tests/multiplier/cross_version_test.go index 8ff8b13b27..ac6eb1e1ea 100644 --- a/tests/multiplier/cross_version_test.go +++ b/tests/multiplier/cross_version_test.go @@ -178,29 +178,41 @@ func TestCrossVersionShouldSkip_WithTwoNodes_DoesNotSkip(t *testing.T) { assert.False(t, newSource().ShouldSkip(actions)) } -func TestCrossVersionShouldSkip_WhenPatchingTheVersionedNode_Skips(t *testing.T) { - // Node 0 is patched, and old-source is the direction that versions node 0. +func TestCrossVersionShouldSkip_WithDocAddedToAllNodes_Skips(t *testing.T) { + // A write with no node set lands on the external node too, which the harness + // cannot observe. actions := action.Actions{ action.RandomNetworkingConfig(), action.RandomNetworkingConfig(), - &action.PatchCollection{NodeID: immutable.Some(0)}, + &action.AddDoc{Doc: `{"Name": "John"}`}, } assert.True(t, oldSource().ShouldSkip(actions)) - assert.False(t, newSource().ShouldSkip(actions), "the other direction leaves node 0 native") + assert.True(t, newSource().ShouldSkip(actions)) } -func TestCrossVersionShouldSkip_WhenPatchingAllNodes_Skips(t *testing.T) { - // A patch with no node set applies everywhere, so it always hits the - // versioned node. +func TestCrossVersionShouldSkip_WithDocUpdatedOnAllNodes_Skips(t *testing.T) { actions := action.Actions{ action.RandomNetworkingConfig(), action.RandomNetworkingConfig(), - &action.PatchCollection{}, + &action.AddDoc{NodeID: immutable.Some(0), Doc: `{"Name": "John"}`}, + &action.UpdateDoc{Doc: `{"Name": "Fred"}`}, } assert.True(t, oldSource().ShouldSkip(actions)) - assert.True(t, newSource().ShouldSkip(actions)) +} + +func TestCrossVersionShouldSkip_WithWritesNamingTheirNode_DoesNotSkip(t *testing.T) { + // The case the multiplier can handle: every write says where it goes. + 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_WithVersionAlreadySet_Skips(t *testing.T) { From 136709b79503abd05fbae95185cb0d928c9873d3 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 6 Aug 2026 19:49:15 +0200 Subject: [PATCH 07/24] Config --- tests/action/node_setup.go | 66 ++++++++++++++++++++- tests/clients/external/wrapper.go | 17 ++++-- tests/clients/external/wrapper_stub_test.go | 6 +- tests/clients/external/wrapper_test.go | 2 +- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index a01c0fc69a..586ace3dcc 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -16,6 +16,7 @@ package action import ( "context" "fmt" + "strings" "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" @@ -65,7 +66,7 @@ func SetupNode( ver string, ) (*state.NodeState, error) { if ver != "" { - return setupExternalNode(s, ver) + return setupExternalNode(s, cfg, ver) } if opts == nil { @@ -251,7 +252,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 @@ -261,7 +262,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 } @@ -271,6 +279,58 @@ 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") + } + + // 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 only --no-searchable-encryption exists") + } else { + flags = append(flags, "--no-searchable-encryption") + } + if 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/clients/external/wrapper.go b/tests/clients/external/wrapper.go index 2480248c63..4c963a6b02 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,19 @@ 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() From 08a7a89b3121c4c8c302d357ad8aec9493ab247f Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 18 Aug 2026 18:59:57 +0200 Subject: [PATCH 08/24] Mark expected docs on external node --- tests/action/eventually.go | 159 ++++++++++++++++ tests/action/utils_events.go | 131 +++++++++++++- tests/integration/events.go | 170 ++++++++++++++---- .../replicator/with_update_add_field_test.go | 17 +- 4 files changed, 435 insertions(+), 42 deletions(-) create mode 100644 tests/action/eventually.go diff --git a/tests/action/eventually.go b/tests/action/eventually.go new file mode 100644 index 0000000000..9cb2de78b3 --- /dev/null +++ b/tests/action/eventually.go @@ -0,0 +1,159 @@ +// 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 + 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/utils_events.go b/tests/action/utils_events.go index 6c509c497c..5c854922aa 100644 --- a/tests/action/utils_events.go +++ b/tests/action/utils_events.go @@ -12,6 +12,7 @@ package action import ( + "fmt" "strconv" "time" @@ -50,7 +51,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 +135,125 @@ 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 CID is read back from the node with a query rather +// than taken from an event, so the nodes downstream wait on the same head they +// would have anyway. +func MarkDocsExpectedOnTargets( + s *state.State, + sourceNodeID int, + collectionIndex int, + docIDs map[string]struct{}, + ident immutable.Option[state.Identity], +) { + for docID := range docIDs { + head, ok := latestCompositeCID(s, sourceNodeID, docID) + if !ok { + continue + } + + // Build the event ourselves, since we cannot read the real one. + 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. +// +// The composite commit is the one a merge event reports, so this is the same +// CID the native path takes from that event. +func latestCompositeCID(s *state.State, nodeID int, docID string) (cid.Cid, bool) { + result := s.Nodes[nodeID].ExecRequest( + s.Ctx, + fmt.Sprintf( + `query { _commits(docID: %q, filter: {fieldName: {_eq: "_C"}}, order: {height: DESC}, limit: 1) { cid } }`, + docID, + ), + ) + 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/integration/events.go b/tests/integration/events.go index 24ab9ed8b9..fce39e84ee 100644 --- a/tests/integration/events.go +++ b/tests/integration/events.go @@ -26,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" ) @@ -163,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)) @@ -297,10 +304,12 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { totalPending += len(cidSet) } - // An external node runs in another process, so its event bus cannot be - // read from here. Poll it for the same CIDs instead. + // 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 { - waitForCommitsOnNode(s, node, pending) + waitForHeadsOnNode(s, node, pending) continue } @@ -343,62 +352,151 @@ func waitForMergeEvents(s *state.State, action WaitForSync) { } } -// errCommitCIDNotFound is returned when querying a CID the node does not hold. -const errCommitCIDNotFound = "cid either does not exist or belong to document" - -// waitForCommitsOnNode waits until every pending CID is present on the node. +// waitForHeadsOnNode waits until every expected head has arrived on the node. // -// It queries the node instead of reading its event bus, so it works for a node -// running in another process. pending maps a doc ID or collection ID to the CIDs -// still expected to arrive. -func waitForCommitsOnNode(s *state.State, node *state.NodeState, pending map[string]map[cid.Cid]struct{}) { - remaining := 0 - for _, cidSet := range pending { - remaining += len(cidSet) +// It asks the node for its commits rather than reading its event bus, so it +// works for a node running in another process. Waiting on the head rather than +// on the document is what makes an update observable: the document exists from +// the first write onwards, but each update brings 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. + if !strings.HasPrefix(key, "bae-") { + continue + } + for c := range cidSet { + heads = append(heads, wanted{key: key, cid: c}) + } } - if remaining == 0 { + if len(heads) == 0 { return } + // A head that is still readable somewhere 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, and waiting for it would never + // finish. This is read once up front, before the target has caught up, so it + // reflects the writer rather than the node being waited on. + 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 := false - for key, cidSet := range pending { - for c := range cidSet { - if !hasCommit(s, node, c) { - missing = true - continue - } - delete(cidSet, c) - node.P2P.ActualDAGHeads[key] = state.DocHeadState{CID: c} + 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, but only + // while the document still exists on the source: a head that deleted + // it is never going to become readable. + 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 !missing { + if len(missing) == 0 { return } if time.Now().After(deadline) { require.Fail(s.T, "timeout waiting for commits to sync to external node", - "node still missing: %v", pending) + "still missing: %v", missing) } time.Sleep(100 * time.Millisecond) } } -// hasCommit reports whether the node holds the block for the given CID. +// 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 a node other than the given one 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. // -// Querying a CID the node does not have yet is an error rather than an empty -// result, so that case reports false and the caller keeps waiting. Any other -// error is a real problem and fails the test. -func hasCommit(s *state.State, node *state.NodeState, target cid.Cid) bool { +// This is 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(cid: %q) { cid } }`, target.String()), + fmt.Sprintf(`query { _commits(docID: %q, cid: %q) { cid } }`, docID, target.String()), ) for _, err := range result.GQL.Errors { - if strings.Contains(err.Error(), errCommitCIDNotFound) { - return false + // A node on an older release can hold a block written against a newer + // collection version without being able to describe it. Nothing can be + // asked about the commit in that case, so the wait falls back to + // comparing the document against the node that wrote it. + // The block is here but the node cannot describe it, so there is nothing + // left to check. Treat it as arrived: waiting longer would never resolve. + // + // This is weaker than matching the head, so a test that updates a + // document this way can read the value from before the update. Comparing + // content instead does not work either, because the two nodes hold + // deliberately different schemas in exactly this case. + if strings.Contains(err.Error(), errCollectionVersionNotFound) { + return true } - require.NoError(s.T, err, "commit query failed on node") + } + // 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) 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 }, }, }, - }, + }), }, } From 6b0fb3f9d2cabb2b5f7341c81b950f684fa1d22f Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 19 Aug 2026 09:35:36 +0200 Subject: [PATCH 09/24] Corrections --- tests/action/eventually_test.go | 154 ++++++++++++++++++ tests/action/node_setup.go | 5 + tests/action/wait_for_peer_events.go | 8 + tests/clients/external/wrapper.go | 1 - .../simple/peer/with_update_add_field_test.go | 17 +- tests/state/state.go | 11 ++ 6 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 tests/action/eventually_test.go diff --git a/tests/action/eventually_test.go b/tests/action/eventually_test.go new file mode 100644 index 0000000000..33ebd2164f --- /dev/null +++ b/tests/action/eventually_test.go @@ -0,0 +1,154 @@ +// 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_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/node_setup.go b/tests/action/node_setup.go index 586ace3dcc..3db785de5b 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -291,6 +291,11 @@ func externalNodeFlags(s *state.State, cfg NodeSetupConfig) (flags []string, uns 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 { 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 4c963a6b02..4fbd7d8f7e 100644 --- a/tests/clients/external/wrapper.go +++ b/tests/clients/external/wrapper.go @@ -118,7 +118,6 @@ func startWrapper(ctx context.Context, t testing.TB, binaryPath string, extraFla // match the configuration a native node would be given. args := []string{"start", "--url", apiURL, - "--p2paddr", "/ip4/127.0.0.1/tcp/0", "--development", "--no-keyring", "--rootdir", rootDir, 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/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 } From 5eb8839cabf0c5b60bb48d359b138548a3cf8c41 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 19 Aug 2026 10:25:55 +0200 Subject: [PATCH 10/24] Adjust tests --- tests/multiplier/cross_version_test.go | 30 +++++++++----------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/multiplier/cross_version_test.go b/tests/multiplier/cross_version_test.go index ac6eb1e1ea..34240bf07d 100644 --- a/tests/multiplier/cross_version_test.go +++ b/tests/multiplier/cross_version_test.go @@ -178,37 +178,27 @@ func TestCrossVersionShouldSkip_WithTwoNodes_DoesNotSkip(t *testing.T) { assert.False(t, newSource().ShouldSkip(actions)) } -func TestCrossVersionShouldSkip_WithDocAddedToAllNodes_Skips(t *testing.T) { - // A write with no node set lands on the external node too, which the harness - // cannot observe. - actions := action.Actions{ - action.RandomNetworkingConfig(), - action.RandomNetworkingConfig(), - &action.AddDoc{Doc: `{"Name": "John"}`}, - } - - assert.True(t, oldSource().ShouldSkip(actions)) - assert.True(t, newSource().ShouldSkip(actions)) -} - -func TestCrossVersionShouldSkip_WithDocUpdatedOnAllNodes_Skips(t *testing.T) { +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{Doc: `{"Name": "Fred"}`}, + &action.UpdateDoc{NodeID: immutable.Some(0), Doc: `{"Name": "Fred"}`}, } - assert.True(t, oldSource().ShouldSkip(actions)) + assert.False(t, oldSource().ShouldSkip(actions)) + assert.False(t, newSource().ShouldSkip(actions)) } -func TestCrossVersionShouldSkip_WithWritesNamingTheirNode_DoesNotSkip(t *testing.T) { - // The case the multiplier can handle: every write says where it goes. +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{NodeID: immutable.Some(0), Doc: `{"Name": "John"}`}, - &action.UpdateDoc{NodeID: immutable.Some(0), Doc: `{"Name": "Fred"}`}, + &action.AddDoc{Doc: `{"Name": "John"}`}, + &action.UpdateDoc{Doc: `{"Name": "Fred"}`}, } assert.False(t, oldSource().ShouldSkip(actions)) From b051371a2972baf3729da3ded2949c3c2908a2b1 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 19 Aug 2026 15:08:37 +0200 Subject: [PATCH 11/24] Fixes --- tests/action/eventually.go | 4 ++++ tests/action/eventually_test.go | 14 ++++++++++++++ tests/action/node_setup.go | 9 +++++++++ tests/action/utils_events.go | 8 +++++--- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/action/eventually.go b/tests/action/eventually.go index 9cb2de78b3..e0cac8ed31 100644 --- a/tests/action/eventually.go +++ b/tests/action/eventually.go @@ -54,6 +54,10 @@ func (a *Eventually) Execute() { } 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 diff --git a/tests/action/eventually_test.go b/tests/action/eventually_test.go index 33ebd2164f..3ebbec7731 100644 --- a/tests/action/eventually_test.go +++ b/tests/action/eventually_test.go @@ -132,6 +132,20 @@ func TestEventually_RealPanic_IsNotSwallowed(t *testing.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. diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index 65e9d86fb0..d3d506aa7c 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -271,6 +271,15 @@ func setupExternalNode(s *state.State, cfg NodeSetupConfig, ver string) (*state. return nil, nil } + // A restart sets up a node that is already on the state, expecting it to + // reopen its store. The wrapper gives the process a new rootdir each time it + // starts, so the node would come back up empty and the test would read that + // as data that never synced. + if s.CurrentSetupNodeID < len(s.Nodes) && s.Nodes[s.CurrentSetupNodeID] != nil { + s.T.Skipf("an external node cannot be restarted: it starts with a new rootdir") + return nil, nil + } + flags, unsupported := externalNodeFlags(s, cfg) if len(unsupported) > 0 { s.T.Skipf("external node cannot be given this test's configuration: %s", diff --git a/tests/action/utils_events.go b/tests/action/utils_events.go index 5c854922aa..a37bd9a466 100644 --- a/tests/action/utils_events.go +++ b/tests/action/utils_events.go @@ -150,10 +150,12 @@ func MarkDocsExpectedOnTargets( 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 instead would record nothing to wait for, and the + // assertions that follow would run against data that never arrived and + // still pass. head, ok := latestCompositeCID(s, sourceNodeID, docID) - if !ok { - continue - } + require.True(s.T, ok, "node %d could not report the head of %s", sourceNodeID, docID) // Build the event ourselves, since we cannot read the real one. evt := event.Update{ From a166e06a2eac30811f99959f4d1ecc73d2645505 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 19 Aug 2026 20:10:59 +0200 Subject: [PATCH 12/24] Explicitly exclude cross version from restart tests --- tests/action/node_setup.go | 9 --------- .../net/simple/peer/with_update_restart_test.go | 8 ++++++++ .../simple/peer_replicator/with_update_restart_test.go | 8 ++++++++ .../net/simple/replicator/with_add_restart_test.go | 8 ++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index d3d506aa7c..65e9d86fb0 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -271,15 +271,6 @@ func setupExternalNode(s *state.State, cfg NodeSetupConfig, ver string) (*state. return nil, nil } - // A restart sets up a node that is already on the state, expecting it to - // reopen its store. The wrapper gives the process a new rootdir each time it - // starts, so the node would come back up empty and the test would read that - // as data that never synced. - if s.CurrentSetupNodeID < len(s.Nodes) && s.Nodes[s.CurrentSetupNodeID] != nil { - s.T.Skipf("an external node cannot be restarted: it starts with a new rootdir") - return nil, nil - } - flags, unsupported := externalNodeFlags(s, cfg) if len(unsupported) > 0 { s.T.Skipf("external node cannot be given this test's configuration: %s", 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(), From f958c8a16ef7ebcf181b74be6936e1a1de7ebac2 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 20 Aug 2026 10:39:36 +0200 Subject: [PATCH 13/24] commit --- tests/action/acp_dac_config.go | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/action/acp_dac_config.go b/tests/action/acp_dac_config.go index edd7a09e14..94a89735e4 100644 --- a/tests/action/acp_dac_config.go +++ b/tests/action/acp_dac_config.go @@ -17,29 +17,17 @@ import ( "github.com/sourcenetwork/defradb/tests/state" ) -const ( - documentACPTypeEnvName = "DEFRA_DOCUMENT_ACP_TYPE" - sourcehubImageEnvName = "DEFRA_SOURCEHUB_IMAGE" -) - -var ( - // 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. - DocumentACPType state.DocumentACPType +const documentACPTypeEnvName = "DEFRA_DOCUMENT_ACP_TYPE" - // sourcehubImage is the container image used to run SourceHub. - sourcehubImage string -) +// 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 } - sourcehubImage = os.Getenv(sourcehubImageEnvName) - if sourcehubImage == "" { - sourcehubImage = "ghcr.io/sourcenetwork/sourcehub:dev" - } } From da0b2614d1773bb59e0503b3dcfebaef5db251bd Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 20 Aug 2026 11:55:51 +0200 Subject: [PATCH 14/24] polish --- tests/action/utils_events.go | 16 +++++------- tests/integration/events.go | 50 ++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/tests/action/utils_events.go b/tests/action/utils_events.go index a37bd9a466..47f42ccc62 100644 --- a/tests/action/utils_events.go +++ b/tests/action/utils_events.go @@ -139,9 +139,8 @@ func waitForUpdateEvents( // node the source syncs to. // // It is the counterpart of [updateNetworkState] for a source node whose events -// cannot be read. The head CID is read back from the node with a query rather -// than taken from an event, so the nodes downstream wait on the same head they -// would have anyway. +// 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, @@ -151,13 +150,12 @@ func MarkDocsExpectedOnTargets( ) { for docID := range docIDs { // The source node wrote this document, so it must be able to report the - // commit. Skipping instead would record nothing to wait for, and the - // assertions that follow would run against data that never arrived and - // still pass. + // 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) require.True(s.T, ok, "node %d could not report the head of %s", sourceNodeID, docID) - // Build the event ourselves, since we cannot read the real one. + // Build the event, since the real one cannot be read. evt := event.Update{ DocID: docID, Cid: head, @@ -210,8 +208,8 @@ func docIndexForID(s *state.State, collectionIndex int, docID string) int { // latestCompositeCID asks the node for the newest composite commit of a // document. // -// The composite commit is the one a merge event reports, so this is the same -// CID the native path takes from that event. +// A merge event reports the composite commit, so this is the same CID the native +// path takes from that event. func latestCompositeCID(s *state.State, nodeID int, docID string) (cid.Cid, bool) { result := s.Nodes[nodeID].ExecRequest( s.Ctx, diff --git a/tests/integration/events.go b/tests/integration/events.go index fce39e84ee..a8f4353258 100644 --- a/tests/integration/events.go +++ b/tests/integration/events.go @@ -354,10 +354,10 @@ 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 rather than reading its event bus, so it -// works for a node running in another process. Waiting on the head rather than -// on the document is what makes an update observable: the document exists from -// the first write onwards, but each update brings a new head. +// 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 @@ -365,8 +365,9 @@ func waitForHeadsOnNode(s *state.State, node *state.NodeState, pending map[strin } var heads []wanted for key, cidSet := range pending { - // A collection level key has no document to ask about. - if !strings.HasPrefix(key, "bae-") { + // 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 { @@ -377,11 +378,11 @@ func waitForHeadsOnNode(s *state.State, node *state.NodeState, pending map[strin return } - // A head that is still readable somewhere 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, and waiting for it would never - // finish. This is read once up front, before the target has caught up, so it - // reflects the writer rather than the node being waited on. + // 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 { @@ -399,9 +400,8 @@ func waitForHeadsOnNode(s *state.State, node *state.NodeState, pending map[strin 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, but only - // while the document still exists on the source: a head that deleted - // it is never going to become readable. + // 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 @@ -423,8 +423,8 @@ func waitForHeadsOnNode(s *state.State, node *state.NodeState, pending map[strin // whose collection version it does not have. const errCollectionVersionNotFound = "failed to get collection by version ID" -// anyNodeHasDocExcept reports whether a node other than the given one still -// holds the document, which stands in for "the writer did not delete it". +// 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 { @@ -440,8 +440,8 @@ func anyNodeHasDocExcept(s *state.State, except *state.NodeState, docID string) // hasDoc reports whether the node can read the given document in any of its // collections. // -// This is weaker than checking the head, so it is only used when the node -// cannot answer about commits at all. +// 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( @@ -478,17 +478,11 @@ func hasCommit(s *state.State, node *state.NodeState, docID string, target cid.C fmt.Sprintf(`query { _commits(docID: %q, cid: %q) { cid } }`, docID, target.String()), ) for _, err := range result.GQL.Errors { - // A node on an older release can hold a block written against a newer - // collection version without being able to describe it. Nothing can be - // asked about the commit in that case, so the wait falls back to - // comparing the document against the node that wrote it. - // The block is here but the node cannot describe it, so there is nothing - // left to check. Treat it as arrived: waiting longer would never resolve. + // 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. // - // This is weaker than matching the head, so a test that updates a - // document this way can read the value from before the update. Comparing - // content instead does not work either, because the two nodes hold - // deliberately different schemas in exactly this case. + // 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 } From f498dc9a91692f696876a29ba67bd8e31b7bf063 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Mon, 24 Aug 2026 21:17:23 +0200 Subject: [PATCH 15/24] polish --- .github/workflows/test-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index c2c2bd282d..02e25f9aff 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -392,7 +392,7 @@ jobs: # 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 downloaded over plain HTTP, so no token is needed. + # 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 From 3f5ade95d2c2942fdb0e3de02d8ef77ee3087a98 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 25 Aug 2026 12:26:44 +0200 Subject: [PATCH 16/24] Fix --- tests/multiplier/cross_version_test.go | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/multiplier/cross_version_test.go b/tests/multiplier/cross_version_test.go index 5cac70952b..458a4a4623 100644 --- a/tests/multiplier/cross_version_test.go +++ b/tests/multiplier/cross_version_test.go @@ -24,6 +24,16 @@ import ( "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} } @@ -79,8 +89,8 @@ func TestCrossVersionApply_OldSource_VersionsFirstNode(t *testing.T) { result := oldSource().Apply(source) require.Len(t, result, 2) - assert.Equal(t, CrossVersionTargetVersion, result[0].(*action.NewNode).Version) - assert.Equal(t, "", result[1].(*action.NewNode).Version) + assert.Equal(t, CrossVersionTargetVersion, nodeAt(t, result, 0).Version) + assert.Equal(t, "", nodeAt(t, result, 1).Version) } func TestCrossVersionApply_NewSource_VersionsLastNode(t *testing.T) { @@ -91,8 +101,8 @@ func TestCrossVersionApply_NewSource_VersionsLastNode(t *testing.T) { result := newSource().Apply(source) require.Len(t, result, 2) - assert.Equal(t, "", result[0].(*action.NewNode).Version) - assert.Equal(t, CrossVersionTargetVersion, result[1].(*action.NewNode).Version) + assert.Equal(t, "", nodeAt(t, result, 0).Version) + assert.Equal(t, CrossVersionTargetVersion, nodeAt(t, result, 1).Version) } func TestCrossVersionApply_WithThreeNodes_VersionsOnlyOne(t *testing.T) { @@ -105,9 +115,9 @@ func TestCrossVersionApply_WithThreeNodes_VersionsOnlyOne(t *testing.T) { result := newSource().Apply(source) require.Len(t, result, 3) - assert.Equal(t, "", result[0].(*action.NewNode).Version) - assert.Equal(t, "", result[1].(*action.NewNode).Version) - assert.Equal(t, CrossVersionTargetVersion, result[2].(*action.NewNode).Version) + 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) { @@ -143,7 +153,7 @@ func TestCrossVersionApply_PreservesNetworkingConfig(t *testing.T) { result := oldSource().Apply(source) - versioned := result[0].(*action.NewNode) + versioned := nodeAt(t, result, 0) assert.NotNil(t, versioned.Network, "networking config must survive the rewrite") } @@ -152,7 +162,7 @@ func TestCrossVersionApply_WithSingleNode_ReturnsSourceUnchanged(t *testing.T) { result := oldSource().Apply(source) - assert.Equal(t, "", result[0].(*action.NewNode).Version) + assert.Equal(t, "", nodeAt(t, result, 0).Version) } func TestCrossVersionShouldSkip_WithSingleNode_Skips(t *testing.T) { From 50c37c0837946bb18069bf1bd40fade8aaa3d679 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 25 Aug 2026 12:50:56 +0200 Subject: [PATCH 17/24] lint --- tests/action/node_setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/action/node_setup.go b/tests/action/node_setup.go index 65e9d86fb0..4dff913964 100644 --- a/tests/action/node_setup.go +++ b/tests/action/node_setup.go @@ -324,7 +324,7 @@ func externalNodeFlags(s *state.State, cfg NodeSetupConfig) (flags []string, uns // 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 only --no-searchable-encryption exists") + unsupported = append(unsupported, "searchable encryption: the test supplies a key, and no flag sets one") } else { flags = append(flags, "--no-searchable-encryption") } From 227dfe74b8eb6b9ea0416c7577aaab2645c23d6f Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 25 Aug 2026 16:21:38 +0200 Subject: [PATCH 18/24] Pass audience to token aud check --- tests/action/identity.go | 9 +++-- tests/integration/identity.go | 9 +++-- tests/state/identity.go | 33 ++++++++++++++--- tests/state/identity_test.go | 69 +++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 tests/state/identity_test.go 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/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/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")) +} From a6935fdbe0f5250f5430289a65e804faec7d2517 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 25 Aug 2026 17:13:35 +0200 Subject: [PATCH 19/24] exclude restarting tests --- .../acp/nac/add_p2p_collection_test.go | 22 ++++++++++ .../acp/nac/add_p2p_document_test.go | 22 ++++++++++ .../acp/nac/add_p2p_replicator_test.go | 22 ++++++++++ .../acp/nac/connect_p2p_peer_test.go | 22 ++++++++++ .../acp/nac/delete_p2p_collection_test.go | 22 ++++++++++ .../acp/nac/delete_p2p_document_test.go | 22 ++++++++++ .../acp/nac/delete_p2p_replicator_test.go | 22 ++++++++++ .../acp/nac/disconnect_p2p_peer_test.go | 22 ++++++++++ .../acp/nac/get_p2p_active_peers_test.go | 22 ++++++++++ .../acp/nac/get_p2p_peer_info_test.go | 22 ++++++++++ .../acp/nac/list_p2p_collection_test.go | 22 ++++++++++ .../acp/nac/list_p2p_document_test.go | 22 ++++++++++ .../acp/nac/list_p2p_replicator_test.go | 22 ++++++++++ .../relation_admin/add_p2p_collection_test.go | 8 ++++ .../relation_admin/add_p2p_document_test.go | 8 ++++ .../relation_admin/add_p2p_replicator_test.go | 8 ++++ .../relation_admin/connect_p2p_peer_test.go | 8 ++++ .../delete_p2p_collection_test.go | 8 ++++ .../delete_p2p_document_test.go | 8 ++++ .../delete_p2p_replicator_test.go | 8 ++++ .../get_p2p_active_peers_test.go | 8 ++++ .../relation_admin/get_p2p_peer_info_test.go | 8 ++++ .../list_p2p_collection_test.go | 8 ++++ .../relation_admin/list_p2p_document_test.go | 8 ++++ .../list_p2p_replicator_test.go | 8 ++++ .../sync_p2p_branchable_collection_test.go | 8 ++++ .../sync_p2p_collection_versions_test.go | 8 ++++ .../relation_admin/sync_p2p_documents_test.go | 8 ++++ .../sync_p2p_branchable_collection_test.go | 22 ++++++++++ .../nac/sync_p2p_collection_versions_test.go | 22 ++++++++++ .../acp/nac/sync_p2p_documents_test.go | 22 ++++++++++ tests/integration/encryption/peer_nac_test.go | 43 +++++++++++++++++++ .../net/simple/replicator/with_add_test.go | 8 ++++ .../net/simple/replicator/with_update_test.go | 15 +++++++ .../searchable_encryption/replicator_test.go | 8 ++++ 35 files changed, 546 insertions(+) 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/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/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_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/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{ From 6d7e8a1b05b608559f60d469d18adaadf685412b Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Tue, 25 Aug 2026 18:16:42 +0200 Subject: [PATCH 20/24] add identity --- tests/action/utils_events.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/action/utils_events.go b/tests/action/utils_events.go index 47f42ccc62..1d4dd23da7 100644 --- a/tests/action/utils_events.go +++ b/tests/action/utils_events.go @@ -21,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" @@ -152,7 +153,7 @@ func MarkDocsExpectedOnTargets( // 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) + 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. @@ -210,13 +211,29 @@ func docIndexForID(s *state.State, collectionIndex int, docID string) int { // // A merge event reports the composite commit, so this is the same CID the native // path takes from that event. -func latestCompositeCID(s *state.State, nodeID int, docID string) (cid.Cid, bool) { +// +// 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 From 31cfda41107b1ae2a785d524fdfbe19c2c00c6fb Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Wed, 26 Aug 2026 16:40:32 +0200 Subject: [PATCH 21/24] skip test --- .../acp/dac/p2p/sync_shared_field_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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(), From a3f9c0062d8a0466288916eef55aa55ee3779fa9 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 27 Aug 2026 00:13:39 +0200 Subject: [PATCH 22/24] More excludes --- .../acp/dac/branchable/peer_test.go | 22 +++++++++++++++++++ tests/integration/index/vector_p2p_test.go | 7 ++++++ tests/integration/node/identity_test.go | 8 +++++++ tests/integration/signature/peer_test.go | 15 +++++++++++++ 4 files changed, 52 insertions(+) diff --git a/tests/integration/acp/dac/branchable/peer_test.go b/tests/integration/acp/dac/branchable/peer_test.go index 34eb104e7d..18648590ab 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,13 @@ 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 collection is private, and the document is written without an identity, so + // the head cannot be read back to work out what the peer should receive. + // https://github.com/sourcenetwork/defradb/issues/5193 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, @@ -95,6 +103,13 @@ func TestACP_P2PBranchableCollectionSyncedWithNodeCollectionAccess_LocalACP(t *t ownerCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ + // The collection is private, and the document is written without an identity, so + // the head cannot be read back to work out what the peer should receive. + // https://github.com/sourcenetwork/defradb/issues/5193 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, @@ -189,6 +204,13 @@ func TestACP_P2PBranchableCollectionSharedReaderCanReadOnPeer_LocalACP(t *testin afterCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ + // The collection is private, and the document is written without an identity, so + // the head cannot be read back to work out what the peer should receive. + // https://github.com/sourcenetwork/defradb/issues/5193 + MultiplierExcludes: []string{ + multiplier.CrossVersionOldSource, + multiplier.CrossVersionNewSource, + }, SupportedDocumentACPTypes: immutable.Some( []state.DocumentACPType{ state.LocalDocumentACPType, diff --git a/tests/integration/index/vector_p2p_test.go b/tests/integration/index/vector_p2p_test.go index 2bfd5fd990..703c0386ea 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 @vectorIndex 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/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/signature/peer_test.go b/tests/integration/signature/peer_test.go index c0cce6d98f..25c4672679 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,13 @@ 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, so the key types this test + // assigns per node are not applied to it. + // 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 +247,13 @@ 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, so the key types this test + // assigns per node are not applied to it. + // 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, From 8e075d9970d5e2f059b81aaf82f6bb94db9e7f5e Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 27 Aug 2026 12:30:11 +0200 Subject: [PATCH 23/24] comment corrections --- .../acp/dac/branchable/peer_test.go | 21 +++++++++++-------- tests/integration/signature/peer_test.go | 10 +++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/integration/acp/dac/branchable/peer_test.go b/tests/integration/acp/dac/branchable/peer_test.go index 18648590ab..3731608c0d 100644 --- a/tests/integration/acp/dac/branchable/peer_test.go +++ b/tests/integration/acp/dac/branchable/peer_test.go @@ -36,9 +36,10 @@ 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 collection is private, and the document is written without an identity, so - // the head cannot be read back to work out what the peer should receive. - // https://github.com/sourcenetwork/defradb/issues/5193 + // 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, @@ -103,9 +104,10 @@ func TestACP_P2PBranchableCollectionSyncedWithNodeCollectionAccess_LocalACP(t *t ownerCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ - // The collection is private, and the document is written without an identity, so - // the head cannot be read back to work out what the peer should receive. - // https://github.com/sourcenetwork/defradb/issues/5193 + // 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, @@ -204,9 +206,10 @@ func TestACP_P2PBranchableCollectionSharedReaderCanReadOnPeer_LocalACP(t *testin afterCid := testUtils.NewUniqueValue() test := testUtils.TestCase{ - // The collection is private, and the document is written without an identity, so - // the head cannot be read back to work out what the peer should receive. - // https://github.com/sourcenetwork/defradb/issues/5193 + // 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, diff --git a/tests/integration/signature/peer_test.go b/tests/integration/signature/peer_test.go index 25c4672679..0677cff310 100644 --- a/tests/integration/signature/peer_test.go +++ b/tests/integration/signature/peer_test.go @@ -140,8 +140,9 @@ 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, so the key types this test - // assigns per node are not applied to it. + // 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, @@ -247,8 +248,9 @@ 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, so the key types this test - // assigns per node are not applied to it. + // 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, From d82513aa2277b15932bb588fd6fcaee7cabbb239 Mon Sep 17 00:00:00 2001 From: Islam Aleiv Date: Thu, 27 Aug 2026 13:02:10 +0200 Subject: [PATCH 24/24] Exclude external if not http client --- tests/integration/apply_multipliers_test.go | 62 +++++++++++++++++++++ tests/integration/utils.go | 27 +++++++++ tests/multiplier/cross_version.go | 8 +++ tests/multiplier/cross_version_test.go | 14 +++++ 4 files changed, 111 insertions(+) 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/utils.go b/tests/integration/utils.go index caaafb1828..e580a46489 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -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. // diff --git a/tests/multiplier/cross_version.go b/tests/multiplier/cross_version.go index b780566f1b..8b461da7e1 100644 --- a/tests/multiplier/cross_version.go +++ b/tests/multiplier/cross_version.go @@ -113,3 +113,11 @@ func nodeActions(actions action.Actions) []*action.NewNode { } 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 index 458a4a4623..c504b0080d 100644 --- a/tests/multiplier/cross_version_test.go +++ b/tests/multiplier/cross_version_test.go @@ -225,3 +225,17 @@ func TestCrossVersionShouldSkip_WithVersionAlreadySet_Skips(t *testing.T) { 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"))) +}