|
4 | 4 | "strings" |
5 | 5 | "testing" |
6 | 6 |
|
| 7 | + "github.com/keboola/keboola-sdk-go/v2/pkg/keboola" |
7 | 8 | "github.com/stretchr/testify/assert" |
8 | 9 | "github.com/stretchr/testify/require" |
9 | 10 |
|
@@ -66,6 +67,99 @@ func TestRelationsMapperLinkRelations(t *testing.T) { |
66 | 67 | }, object2.Local.Relations) |
67 | 68 | } |
68 | 69 |
|
| 70 | +// TestRelationsMapperVariablesSharedAcrossConsumers verifies that when a variables config |
| 71 | +// is loaded before its consumers in changes.Loaded(), the two-pass approach still produces |
| 72 | +// exactly one warning instead of crashing with "multiple parents defined by relations". |
| 73 | +// It also exercises the > 1 guard in VariablesValuesForRelation.NewOtherSideRelation by |
| 74 | +// including a values row loaded after both consumers so that, during Pass 1, the variables |
| 75 | +// config already holds two variablesFor relations when linkRelations(valuesRow) runs. |
| 76 | +func TestRelationsMapperVariablesSharedAcrossConsumers(t *testing.T) { |
| 77 | + t.Parallel() |
| 78 | + state, d := createStateWithMapper(t) |
| 79 | + logger := d.DebugLogger() |
| 80 | + |
| 81 | + branchID := keboola.BranchID(1) |
| 82 | + consumerCompID := keboola.ComponentID("ex-generic-v2") |
| 83 | + |
| 84 | + // Variables config (Y) — added to state and loaded FIRST to exercise the ordering |
| 85 | + // that previously caused a fatal "multiple parents" crash in PathsGenerator. |
| 86 | + varsKey := model.ConfigKey{BranchID: branchID, ComponentID: keboola.VariablesComponentID, ID: "vars"} |
| 87 | + varsConfig := &model.ConfigState{ |
| 88 | + ConfigManifest: &model.ConfigManifest{ConfigKey: varsKey}, |
| 89 | + Remote: &model.Config{ConfigKey: varsKey}, |
| 90 | + } |
| 91 | + require.NoError(t, state.Set(varsConfig)) |
| 92 | + |
| 93 | + // Consumer 1 — variablesFrom and variablesValuesFrom relations pointing to varsKey. |
| 94 | + // VariablesValuesFromRelation causes linkRelations(consumer1) to add a VariablesValuesFor |
| 95 | + // relation to the values row, which in turn exercises the > 1 guard when |
| 96 | + // linkRelations(valuesRow) runs later in Pass 1. |
| 97 | + valuesRowID := keboola.RowID("val1") |
| 98 | + consumer1Key := model.ConfigKey{BranchID: branchID, ComponentID: consumerCompID, ID: "consumer1"} |
| 99 | + consumer1 := &model.ConfigState{ |
| 100 | + ConfigManifest: &model.ConfigManifest{ConfigKey: consumer1Key}, |
| 101 | + Remote: &model.Config{ |
| 102 | + ConfigKey: consumer1Key, |
| 103 | + Relations: model.Relations{ |
| 104 | + &model.VariablesFromRelation{VariablesID: varsKey.ID}, |
| 105 | + &model.VariablesValuesFromRelation{VariablesValuesID: valuesRowID}, |
| 106 | + }, |
| 107 | + }, |
| 108 | + } |
| 109 | + require.NoError(t, state.Set(consumer1)) |
| 110 | + |
| 111 | + // Consumer 2 — also variablesFrom relation pointing to the same varsKey. |
| 112 | + consumer2Key := model.ConfigKey{BranchID: branchID, ComponentID: consumerCompID, ID: "consumer2"} |
| 113 | + consumer2 := &model.ConfigState{ |
| 114 | + ConfigManifest: &model.ConfigManifest{ConfigKey: consumer2Key}, |
| 115 | + Remote: &model.Config{ |
| 116 | + ConfigKey: consumer2Key, |
| 117 | + Relations: model.Relations{ |
| 118 | + &model.VariablesFromRelation{VariablesID: varsKey.ID}, |
| 119 | + }, |
| 120 | + }, |
| 121 | + } |
| 122 | + require.NoError(t, state.Set(consumer2)) |
| 123 | + |
| 124 | + // Values row — loaded LAST so that when linkRelations(valuesRow) runs in Pass 1, the |
| 125 | + // variables config already holds 2 variablesFor relations (added by consumer1 and |
| 126 | + // consumer2). The > 1 guard in VariablesValuesForRelation.NewOtherSideRelation fires |
| 127 | + // and returns (nil, nil, nil), preventing a duplicate "invalid config Y" error. |
| 128 | + valuesRowKey := model.ConfigRowKey{BranchID: branchID, ComponentID: keboola.VariablesComponentID, ConfigID: varsKey.ID, ID: valuesRowID} |
| 129 | + valuesRow := &model.ConfigRowState{ |
| 130 | + ConfigRowManifest: &model.ConfigRowManifest{ConfigRowKey: valuesRowKey}, |
| 131 | + Remote: &model.ConfigRow{ConfigRowKey: valuesRowKey}, |
| 132 | + } |
| 133 | + require.NoError(t, state.Set(valuesRow)) |
| 134 | + |
| 135 | + // Variables config is first in Loaded(), values row is last — both orderings that |
| 136 | + // previously caused problems are exercised in a single pass. |
| 137 | + changes := model.NewRemoteChanges() |
| 138 | + changes.AddLoaded(varsConfig) |
| 139 | + changes.AddLoaded(consumer1) |
| 140 | + changes.AddLoaded(consumer2) |
| 141 | + changes.AddLoaded(valuesRow) |
| 142 | + |
| 143 | + // Must return nil — the duplicate is a warning, not a fatal error. |
| 144 | + require.NoError(t, state.Mapper().AfterRemoteOperation(t.Context(), changes)) |
| 145 | + |
| 146 | + // Exactly one warning about the duplicate variablesFor relation. |
| 147 | + allTxt := logger.AllMessagesTxt() |
| 148 | + assert.Equal(t, 1, strings.Count(allTxt, `Only one relation "variablesFor" expected, but found 2`)) |
| 149 | + assert.Empty(t, logger.ErrorMessages()) |
| 150 | + |
| 151 | + // The > 1 guard in VariablesValuesForRelation.NewOtherSideRelation prevented |
| 152 | + // linkRelations(valuesRow) from adding a VariablesValuesFromRelation to any consumer. |
| 153 | + // consumer1 retains its original single VariablesValuesFromRelation (no duplicate was |
| 154 | + // added from the values-row side), and consumer2 has none at all. |
| 155 | + assert.Len(t, consumer1.Remote.Relations.GetByType(model.VariablesValuesFromRelType), 1) |
| 156 | + assert.Empty(t, consumer2.Remote.Relations.GetByType(model.VariablesValuesFromRelType)) |
| 157 | + |
| 158 | + // AfterLocalOperation is structurally identical to AfterRemoteOperation (same two-pass |
| 159 | + // logic). It is not re-tested here because the shared-variables scenario only arises |
| 160 | + // from the remote API path, where object order is non-deterministic. |
| 161 | +} |
| 162 | + |
69 | 163 | func TestRelationsMapperOtherSideMissing(t *testing.T) { |
70 | 164 | t.Parallel() |
71 | 165 | state, d := createStateWithMapper(t) |
|
0 commit comments