From 27f496191663234c6816a9144d58b41953de1d2b Mon Sep 17 00:00:00 2001 From: patrickmann Date: Mon, 6 Jul 2026 10:29:03 +0200 Subject: [PATCH 1/5] Tolerate unresolvable stream references during content pack install Installing a content pack could fail with "Missing Stream for widget entity" when a view referenced a stream that could not be resolved to a native stream. The view-side resolvers threw a ContentPackException, which aborted and rolled back the entire content pack installation. This was asymmetric with the export side (WidgetDTO.toContentPackEntity), which already silently drops stream references it cannot map. Make the three import-time view resolvers behave the same way: skip an unresolvable stream reference with a warning instead of throwing, so a single dangling reference no longer blocks the whole install. The wrong-type branch (a non-null, non-Stream value) still throws, since that indicates a genuinely corrupt entity map rather than a missing reference. Affected resolvers: - WidgetEntity.toNativeEntity - SearchTypeEntity.mappedStreams - QueryEntity.shallowMappedFilter Co-Authored-By: Claude Opus 4.8 (1M context) --- .../model/entities/QueryEntity.java | 15 +++- .../model/entities/SearchTypeEntity.java | 40 +++++++--- .../model/entities/WidgetEntity.java | 40 +++++++--- .../model/entities/QueryEntityTest.java | 16 ++++ .../model/entities/SearchTypeEntityTest.java | 62 ++++++++++++++++ .../model/entities/WidgetEntityTest.java | 74 +++++++++++++++++++ 6 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java create mode 100644 graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java index 97bb3cfdb8c3..cd60ba09f6a0 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java @@ -38,6 +38,8 @@ import org.graylog2.contentpacks.model.entities.references.ValueReference; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import org.graylog2.plugin.streams.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -61,6 +63,8 @@ @JsonDeserialize(builder = QueryEntity.Builder.class) public abstract class QueryEntity implements NativeEntityConverter { + private static final Logger LOG = LoggerFactory.getLogger(QueryEntity.class); + @JsonProperty public abstract String id(); @@ -152,13 +156,18 @@ private Filter shallowMappedFilter(Map nativeEntities) final StreamFilter streamFilter = (StreamFilter) filter; final Stream stream = (Stream) resolveStreamEntityObject(streamFilter.streamId(), nativeEntities); if (Objects.isNull(stream)) { - throw new ContentPackException("Could not find matching stream id: " + - streamFilter.streamId()); + // Skip a dangling stream reference instead of aborting the whole content pack + // installation. This mirrors the export side, which also drops unresolvable references. + LOG.warn("Skipping unresolvable stream reference <{}> in query filter for query <{}> during content pack installation", + streamFilter.streamId(), id()); + return null; } return streamFilter.toBuilder().streamId(stream.getId()).build(); } return filter; - }).collect(Collectors.toSet()); + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); return optFilter.toGenericBuilder().filters(newFilters).build(); }) .orElse(null); diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java index f83fe6238e66..560708240ab1 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java @@ -29,6 +29,8 @@ import org.graylog2.contentpacks.exceptions.ContentPackException; import org.graylog2.contentpacks.model.entities.references.ValueReference; import org.graylog2.plugin.streams.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Collections; @@ -49,6 +51,8 @@ defaultImpl = SearchTypeEntity.Fallback.class) @JsonAutoDetect public interface SearchTypeEntity extends NativeEntityConverter { + Logger LOG = LoggerFactory.getLogger(SearchTypeEntity.class); + String TYPE_FIELD = "type"; String FIELD_SEARCH_FILTERS = "filters"; @@ -207,17 +211,29 @@ public SearchType toNativeEntity(Map parameters, Map mappedStreams(Map nativeEntities) { return streams().stream() - .map(id -> resolveStreamEntityObject(id, nativeEntities)) - .map(object -> { - if (object == null) { - throw new ContentPackException("Missing Stream for event definition"); - } else if (object instanceof Stream) { - Stream stream = (Stream) object; - return stream.getId(); - } else { - throw new ContentPackException( - "Invalid type for stream Stream for event definition: " + object.getClass()); - } - }).collect(Collectors.toSet()); + .map(id -> resolveStreamReference(id, nativeEntities)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + } + + /** + * Resolves a single stream reference from the search type's {@code streams} set to the native stream id. + *

+ * A reference that cannot be resolved (e.g. a dangling reference left over in an exported content pack) is + * skipped with a warning rather than aborting the whole content pack installation. This mirrors the export + * side, which also silently drops stream references it cannot map. + */ + default Optional resolveStreamReference(String id, Map nativeEntities) { + final Object object = resolveStreamEntityObject(id, nativeEntities); + if (object == null) { + LOG.warn("Skipping unresolvable stream reference <{}> for search type <{}> during content pack installation", id, id()); + return Optional.empty(); + } else if (object instanceof Stream) { + return Optional.of(((Stream) object).getId()); + } else { + throw new ContentPackException( + "Invalid type for stream Stream for search type: " + object.getClass()); + } } } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java index 9320de67c7e8..a1b688c65778 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java @@ -51,6 +51,8 @@ import org.graylog2.contentpacks.model.entities.references.ValueReference; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import org.graylog2.plugin.streams.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Collections; @@ -68,6 +70,8 @@ @AutoValue @JsonDeserialize(builder = WidgetEntity.Builder.class) public abstract class WidgetEntity implements NativeEntityConverter { + private static final Logger LOG = LoggerFactory.getLogger(WidgetEntity.class); + public static final String FIELD_ID = "id"; public static final String FIELD_TYPE = "type"; public static final String FIELD_FILTER = "filter"; @@ -181,17 +185,10 @@ public WidgetDTO toNativeEntity(Map parameters, Map filter.toNativeEntity(parameters, nativeEntities)).toList()) .id(this.id()) .streams(this.streams().stream() - .map(id -> resolveStreamEntityObject(id, nativeEntities)) - .map(object -> { - if (object == null) { - throw new ContentPackException("Missing Stream for widget entity"); - } else if (object instanceof final Stream stream) { - return stream.getId(); - } else { - throw new ContentPackException( - "Invalid type for stream Stream for event definition: " + object.getClass()); - } - }).collect(Collectors.toSet())) + .map(id -> resolveStreamReference(id, nativeEntities)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet())) .streamCategories(this.streamCategories()) .type(this.type()); if (this.query().isPresent()) { @@ -203,6 +200,27 @@ public WidgetDTO toNativeEntity(Map parameters, Map + * A reference that cannot be resolved (e.g. a dangling reference left over in an exported content pack) is + * skipped with a warning rather than aborting the whole content pack installation. This mirrors the export + * side ({@link org.graylog.plugins.views.search.views.WidgetDTO#toContentPackEntity}), which also silently + * drops stream references it cannot map. + */ + private Optional resolveStreamReference(String id, Map nativeEntities) { + final Object object = resolveStreamEntityObject(id, nativeEntities); + if (object == null) { + LOG.warn("Skipping unresolvable stream reference <{}> for widget <{}> during content pack installation", id, id()); + return Optional.empty(); + } else if (object instanceof final Stream stream) { + return Optional.of(stream.getId()); + } else { + throw new ContentPackException( + "Invalid type for stream Stream for widget entity: " + object.getClass()); + } + } + @Override public void resolveForInstallation(EntityV1 entity, Map parameters, diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java index 008255c0bb7f..7afc5e17320b 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java @@ -18,6 +18,8 @@ import com.google.common.collect.ImmutableList; import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; +import org.graylog.plugins.views.search.filter.OrFilter; +import org.graylog.plugins.views.search.filter.StreamFilter; import org.graylog.plugins.views.search.searchfilters.model.DBSearchFilter; import org.graylog.plugins.views.search.searchfilters.model.InlineQueryStringSearchFilter; import org.graylog.plugins.views.search.searchfilters.model.ReferencedQueryStringSearchFilter; @@ -76,6 +78,20 @@ public void testLoadsSearchFiltersCollectionFromContentPack() { .isEqualTo(expectedSearchFilters); } + @Test + public void dropsUnresolvableStreamFilterInsteadOfFailing() { + final QueryEntity query = QueryEntity.Builder + .createWithDefaults() + .id("nvmd") + .timerange(RelativeRange.allTime()) + .query(ElasticsearchQueryString.empty()) + .filter(OrFilter.or(StreamFilter.ofId("missing-stream-id"))) + .build(); + + assertThat(query.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()).filter().filters()) + .noneMatch(f -> f instanceof StreamFilter); + } + private class TestDBSearchFilter implements DBSearchFilter { String id; diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java new file mode 100644 index 000000000000..304497f9089f --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.contentpacks.model.entities; + +import com.google.common.collect.ImmutableSet; +import org.graylog.plugins.views.search.SearchType; +import org.graylog2.contentpacks.model.ModelTypes; +import org.graylog2.plugin.streams.Stream; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SearchTypeEntityTest { + + @Test + public void dropsUnresolvableStreamReferenceInsteadOfFailing() { + final MessageListEntity searchType = MessageListEntity.builder() + .id("search-type-id") + .streams(ImmutableSet.of("missing-stream-id")) + .build(); + + final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()); + + assertThat(nativeEntity.streams()).isEmpty(); + } + + @Test + public void keepsResolvableStreamReference() { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn("native-stream-id"); + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); + + final MessageListEntity searchType = MessageListEntity.builder() + .id("search-type-id") + .streams(ImmutableSet.of("cp-stream-id")) + .build(); + + final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), nativeEntities); + + assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java new file mode 100644 index 000000000000..e63440c639d0 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.contentpacks.model.entities; + +import com.google.common.collect.ImmutableSet; +import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; +import org.graylog.plugins.views.search.views.WidgetDTO; +import org.graylog.plugins.views.search.views.widgets.messagelist.MessageListConfigDTO; +import org.graylog2.contentpacks.model.ModelTypes; +import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; +import org.graylog2.plugin.streams.Stream; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WidgetEntityTest { + + private WidgetEntity.Builder widgetBuilder(Set streams) { + return WidgetEntity.builder() + .id("widget-id") + .type(MessageListConfigDTO.NAME) + .filters(Collections.emptyList()) + .timerange(KeywordRange.create("last 5 minutes", "Etc/UTC")) + .query(ElasticsearchQueryString.of("*")) + .streams(streams) + .config(MessageListConfigDTO.Builder.builder() + .fields(ImmutableSet.of()) + .showMessageRow(false) + .build()); + } + + @Test + public void dropsUnresolvableStreamReferenceInsteadOfFailing() { + final WidgetEntity widget = widgetBuilder(ImmutableSet.of("missing-stream-id")).build(); + + final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()); + + assertThat(nativeEntity.streams()).isEmpty(); + } + + @Test + public void keepsResolvableStreamReference() { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn("native-stream-id"); + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); + + final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id")).build(); + + final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), nativeEntities); + + assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); + } +} From 224c101494eb91bd416a18ed8cfe59a8a57319d8 Mon Sep 17 00:00:00 2001 From: patrickmann Date: Mon, 6 Jul 2026 10:29:38 +0200 Subject: [PATCH 2/5] Add changelog for #26581 Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog/unreleased/pr-26581.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/unreleased/pr-26581.toml diff --git a/changelog/unreleased/pr-26581.toml b/changelog/unreleased/pr-26581.toml new file mode 100644 index 000000000000..ccf88cfa9774 --- /dev/null +++ b/changelog/unreleased/pr-26581.toml @@ -0,0 +1,5 @@ +type = "fixed" +message = "Fix content pack installation failing with \"Missing Stream for widget entity\" when a view references a stream that cannot be resolved. The unresolvable stream reference is now skipped with a warning instead of aborting the whole installation." + +issues = ["Graylog2/graylog-plugin-enterprise#12987"] +pulls = ["26581"] From 7644c4c67eadba295841bb1447fa10ba767aa0fe Mon Sep 17 00:00:00 2001 From: patrickmann Date: Mon, 6 Jul 2026 12:45:44 +0200 Subject: [PATCH 3/5] unit tests --- .../model/entities/QueryEntity.java | 12 +++-- .../model/entities/SearchTypeEntity.java | 8 ++-- .../model/entities/WidgetEntity.java | 8 ++-- .../model/entities/QueryEntityTest.java | 48 +++++++++++++++++++ .../model/entities/SearchTypeEntityTest.java | 35 ++++++++++++++ .../model/entities/WidgetEntityTest.java | 29 +++++++++++ 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java index cd60ba09f6a0..918798e5d14d 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java @@ -154,15 +154,21 @@ private Filter shallowMappedFilter(Map nativeEntities) .map(filter -> { if (filter.type().matches(StreamFilter.NAME)) { final StreamFilter streamFilter = (StreamFilter) filter; - final Stream stream = (Stream) resolveStreamEntityObject(streamFilter.streamId(), nativeEntities); - if (Objects.isNull(stream)) { + final Object object = resolveStreamEntityObject(streamFilter.streamId(), nativeEntities); + if (object == null) { // Skip a dangling stream reference instead of aborting the whole content pack // installation. This mirrors the export side, which also drops unresolvable references. LOG.warn("Skipping unresolvable stream reference <{}> in query filter for query <{}> during content pack installation", streamFilter.streamId(), id()); return null; + } else if (object instanceof final Stream stream) { + return streamFilter.toBuilder().streamId(stream.getId()).build(); + } else { + // A non-null, non-Stream value indicates a corrupt entity map rather than a + // missing reference, so abort the installation like the other view resolvers. + throw new ContentPackException( + "Invalid type for stream Stream for query filter: " + object.getClass()); } - return streamFilter.toBuilder().streamId(stream.getId()).build(); } return filter; }) diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java index 560708240ab1..733ad9f359b8 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java @@ -211,7 +211,7 @@ public SearchType toNativeEntity(Map parameters, Map mappedStreams(Map nativeEntities) { return streams().stream() - .map(id -> resolveStreamReference(id, nativeEntities)) + .map(streamId -> resolveStreamReference(streamId, nativeEntities)) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); @@ -224,10 +224,10 @@ default Set mappedStreams(Map nativeEntities) * skipped with a warning rather than aborting the whole content pack installation. This mirrors the export * side, which also silently drops stream references it cannot map. */ - default Optional resolveStreamReference(String id, Map nativeEntities) { - final Object object = resolveStreamEntityObject(id, nativeEntities); + default Optional resolveStreamReference(String streamId, Map nativeEntities) { + final Object object = resolveStreamEntityObject(streamId, nativeEntities); if (object == null) { - LOG.warn("Skipping unresolvable stream reference <{}> for search type <{}> during content pack installation", id, id()); + LOG.warn("Skipping unresolvable stream reference <{}> for search type <{}> during content pack installation", streamId, id()); return Optional.empty(); } else if (object instanceof Stream) { return Optional.of(((Stream) object).getId()); diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java index a1b688c65778..d0b35937a167 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java @@ -185,7 +185,7 @@ public WidgetDTO toNativeEntity(Map parameters, Map filter.toNativeEntity(parameters, nativeEntities)).toList()) .id(this.id()) .streams(this.streams().stream() - .map(id -> resolveStreamReference(id, nativeEntities)) + .map(streamId -> resolveStreamReference(streamId, nativeEntities)) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet())) @@ -208,10 +208,10 @@ public WidgetDTO toNativeEntity(Map parameters, Map resolveStreamReference(String id, Map nativeEntities) { - final Object object = resolveStreamEntityObject(id, nativeEntities); + private Optional resolveStreamReference(String streamId, Map nativeEntities) { + final Object object = resolveStreamEntityObject(streamId, nativeEntities); if (object == null) { - LOG.warn("Skipping unresolvable stream reference <{}> for widget <{}> during content pack installation", id, id()); + LOG.warn("Skipping unresolvable stream reference <{}> for widget <{}> during content pack installation", streamId, id()); return Optional.empty(); } else if (object instanceof final Stream stream) { return Optional.of(stream.getId()); diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java index 7afc5e17320b..a0b165b62274 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java @@ -17,6 +17,7 @@ package org.graylog2.contentpacks.model.entities; import com.google.common.collect.ImmutableList; +import org.graylog.plugins.views.search.Filter; import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; import org.graylog.plugins.views.search.filter.OrFilter; import org.graylog.plugins.views.search.filter.StreamFilter; @@ -24,14 +25,20 @@ import org.graylog.plugins.views.search.searchfilters.model.InlineQueryStringSearchFilter; import org.graylog.plugins.views.search.searchfilters.model.ReferencedQueryStringSearchFilter; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; +import org.graylog2.contentpacks.exceptions.ContentPackException; import org.graylog2.contentpacks.model.ModelTypes; import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; +import org.graylog2.plugin.streams.Stream; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class QueryEntityTest { @@ -92,6 +99,47 @@ public void dropsUnresolvableStreamFilterInsteadOfFailing() { .noneMatch(f -> f instanceof StreamFilter); } + @Test + public void keepsResolvableStreamFilterAndDropsUnresolvable() { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn("native-stream-id"); + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); + + final QueryEntity query = QueryEntity.Builder + .createWithDefaults() + .id("nvmd") + .timerange(RelativeRange.allTime()) + .query(ElasticsearchQueryString.empty()) + .filter(OrFilter.or(StreamFilter.ofId("cp-stream-id"), StreamFilter.ofId("missing-stream-id"))) + .build(); + + final Set mappedFilters = + query.toNativeEntity(Collections.emptyMap(), nativeEntities).filter().filters(); + + assertThat(mappedFilters).hasSize(1); + assertThat(((StreamFilter) mappedFilters.iterator().next()).streamId()).isEqualTo("native-stream-id"); + } + + @Test + public void failsOnWrongTypeStreamReference() { + // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); + + final QueryEntity query = QueryEntity.Builder + .createWithDefaults() + .id("nvmd") + .timerange(RelativeRange.allTime()) + .query(ElasticsearchQueryString.empty()) + .filter(OrFilter.or(StreamFilter.ofId("cp-stream-id"))) + .build(); + + assertThatThrownBy(() -> query.toNativeEntity(Collections.emptyMap(), nativeEntities)) + .isInstanceOf(ContentPackException.class) + .hasMessageContaining("Invalid type for stream"); + } + private class TestDBSearchFilter implements DBSearchFilter { String id; diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java index 304497f9089f..3347411be0ca 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableSet; import org.graylog.plugins.views.search.SearchType; +import org.graylog2.contentpacks.exceptions.ContentPackException; import org.graylog2.contentpacks.model.ModelTypes; import org.graylog2.plugin.streams.Stream; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -59,4 +61,37 @@ public void keepsResolvableStreamReference() { assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); } + + @Test + public void dropsOnlyUnresolvableStreamReferences() { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn("native-stream-id"); + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); + + final MessageListEntity searchType = MessageListEntity.builder() + .id("search-type-id") + .streams(ImmutableSet.of("cp-stream-id", "missing-stream-id")) + .build(); + + final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), nativeEntities); + + assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); + } + + @Test + public void failsOnWrongTypeStreamReference() { + // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); + + final MessageListEntity searchType = MessageListEntity.builder() + .id("search-type-id") + .streams(ImmutableSet.of("cp-stream-id")) + .build(); + + assertThatThrownBy(() -> searchType.toNativeEntity(Collections.emptyMap(), nativeEntities)) + .isInstanceOf(ContentPackException.class) + .hasMessageContaining("Invalid type for stream"); + } } diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java index e63440c639d0..e27b46d56742 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java @@ -20,6 +20,7 @@ import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; import org.graylog.plugins.views.search.views.WidgetDTO; import org.graylog.plugins.views.search.views.widgets.messagelist.MessageListConfigDTO; +import org.graylog2.contentpacks.exceptions.ContentPackException; import org.graylog2.contentpacks.model.ModelTypes; import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; import org.graylog2.plugin.streams.Stream; @@ -30,6 +31,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -71,4 +73,31 @@ public void keepsResolvableStreamReference() { assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); } + + @Test + public void dropsOnlyUnresolvableStreamReferences() { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn("native-stream-id"); + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); + + final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id", "missing-stream-id")).build(); + + final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), nativeEntities); + + assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); + } + + @Test + public void failsOnWrongTypeStreamReference() { + // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); + + final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id")).build(); + + assertThatThrownBy(() -> widget.toNativeEntity(Collections.emptyMap(), nativeEntities)) + .isInstanceOf(ContentPackException.class) + .hasMessageContaining("Invalid type for stream"); + } } From 5d201406b47274c49f444702279d5cdbe64be1f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:12:33 +0000 Subject: [PATCH 4/5] Fix review feedback on stream type errors --- .../org/graylog2/contentpacks/model/entities/QueryEntity.java | 4 +++- .../contentpacks/model/entities/SearchTypeEntity.java | 3 ++- .../graylog2/contentpacks/model/entities/WidgetEntity.java | 3 ++- .../graylog2/contentpacks/model/entities/QueryEntityTest.java | 3 ++- .../contentpacks/model/entities/SearchTypeEntityTest.java | 3 ++- .../contentpacks/model/entities/WidgetEntityTest.java | 3 ++- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java index 918798e5d14d..fac1554a4ee8 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/QueryEntity.java @@ -56,6 +56,7 @@ import static com.google.common.collect.ImmutableSortedSet.of; import static java.util.stream.Collectors.toSet; import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject; +import static org.graylog2.shared.utilities.StringUtils.f; @AutoValue @JsonAutoDetect @@ -167,7 +168,8 @@ private Filter shallowMappedFilter(Map nativeEntities) // A non-null, non-Stream value indicates a corrupt entity map rather than a // missing reference, so abort the installation like the other view resolvers. throw new ContentPackException( - "Invalid type for stream Stream for query filter: " + object.getClass()); + f("Invalid type for stream <%s> in query <%s>: %s", + streamFilter.streamId(), id(), object.getClass())); } } return filter; diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java index 733ad9f359b8..ce2c085df668 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/SearchTypeEntity.java @@ -42,6 +42,7 @@ import java.util.stream.Collectors; import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject; +import static org.graylog2.shared.utilities.StringUtils.f; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -233,7 +234,7 @@ default Optional resolveStreamReference(String streamId, Map in search type <%s>: %s", streamId, id(), object.getClass())); } } } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java index d0b35937a167..c938dbc53511 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/WidgetEntity.java @@ -66,6 +66,7 @@ import java.util.stream.Collectors; import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject; +import static org.graylog2.shared.utilities.StringUtils.f; @AutoValue @JsonDeserialize(builder = WidgetEntity.Builder.class) @@ -217,7 +218,7 @@ private Optional resolveStreamReference(String streamId, Map in widget <%s>: %s", streamId, id(), object.getClass())); } } diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java index a0b165b62274..925ea8b22b50 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/QueryEntityTest.java @@ -137,7 +137,8 @@ public void failsOnWrongTypeStreamReference() { assertThatThrownBy(() -> query.toNativeEntity(Collections.emptyMap(), nativeEntities)) .isInstanceOf(ContentPackException.class) - .hasMessageContaining("Invalid type for stream"); + .hasMessageContaining("cp-stream-id") + .hasMessageContaining("nvmd"); } private class TestDBSearchFilter implements DBSearchFilter { diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java index 3347411be0ca..b0caa6b9de2f 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java @@ -92,6 +92,7 @@ public void failsOnWrongTypeStreamReference() { assertThatThrownBy(() -> searchType.toNativeEntity(Collections.emptyMap(), nativeEntities)) .isInstanceOf(ContentPackException.class) - .hasMessageContaining("Invalid type for stream"); + .hasMessageContaining("cp-stream-id") + .hasMessageContaining("search-type-id"); } } diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java index e27b46d56742..8f18b8b47cce 100644 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java @@ -98,6 +98,7 @@ public void failsOnWrongTypeStreamReference() { assertThatThrownBy(() -> widget.toNativeEntity(Collections.emptyMap(), nativeEntities)) .isInstanceOf(ContentPackException.class) - .hasMessageContaining("Invalid type for stream"); + .hasMessageContaining("cp-stream-id") + .hasMessageContaining("widget-id"); } } From 6c4d3737a430f187e38200eb34ac583c07a73a69 Mon Sep 17 00:00:00 2001 From: patrickmann Date: Mon, 6 Jul 2026 13:56:15 +0200 Subject: [PATCH 5/5] refactor tests --- .../model/entities/SearchTypeEntityTest.java | 98 -------------- .../StreamReferenceResolutionTest.java | 122 ++++++++++++++++++ .../model/entities/WidgetEntityTest.java | 104 --------------- 3 files changed, 122 insertions(+), 202 deletions(-) delete mode 100644 graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java create mode 100644 graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/StreamReferenceResolutionTest.java delete mode 100644 graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java deleted file mode 100644 index b0caa6b9de2f..000000000000 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/SearchTypeEntityTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -package org.graylog2.contentpacks.model.entities; - -import com.google.common.collect.ImmutableSet; -import org.graylog.plugins.views.search.SearchType; -import org.graylog2.contentpacks.exceptions.ContentPackException; -import org.graylog2.contentpacks.model.ModelTypes; -import org.graylog2.plugin.streams.Stream; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class SearchTypeEntityTest { - - @Test - public void dropsUnresolvableStreamReferenceInsteadOfFailing() { - final MessageListEntity searchType = MessageListEntity.builder() - .id("search-type-id") - .streams(ImmutableSet.of("missing-stream-id")) - .build(); - - final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()); - - assertThat(nativeEntity.streams()).isEmpty(); - } - - @Test - public void keepsResolvableStreamReference() { - final Stream stream = mock(Stream.class); - when(stream.getId()).thenReturn("native-stream-id"); - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); - - final MessageListEntity searchType = MessageListEntity.builder() - .id("search-type-id") - .streams(ImmutableSet.of("cp-stream-id")) - .build(); - - final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), nativeEntities); - - assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); - } - - @Test - public void dropsOnlyUnresolvableStreamReferences() { - final Stream stream = mock(Stream.class); - when(stream.getId()).thenReturn("native-stream-id"); - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); - - final MessageListEntity searchType = MessageListEntity.builder() - .id("search-type-id") - .streams(ImmutableSet.of("cp-stream-id", "missing-stream-id")) - .build(); - - final SearchType nativeEntity = searchType.toNativeEntity(Collections.emptyMap(), nativeEntities); - - assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); - } - - @Test - public void failsOnWrongTypeStreamReference() { - // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); - - final MessageListEntity searchType = MessageListEntity.builder() - .id("search-type-id") - .streams(ImmutableSet.of("cp-stream-id")) - .build(); - - assertThatThrownBy(() -> searchType.toNativeEntity(Collections.emptyMap(), nativeEntities)) - .isInstanceOf(ContentPackException.class) - .hasMessageContaining("cp-stream-id") - .hasMessageContaining("search-type-id"); - } -} diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/StreamReferenceResolutionTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/StreamReferenceResolutionTest.java new file mode 100644 index 000000000000..1df4fc84fea6 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/StreamReferenceResolutionTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.contentpacks.model.entities; + +import com.google.common.collect.ImmutableSet; +import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; +import org.graylog.plugins.views.search.views.widgets.messagelist.MessageListConfigDTO; +import org.graylog2.contentpacks.exceptions.ContentPackException; +import org.graylog2.contentpacks.model.ModelTypes; +import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; +import org.graylog2.plugin.streams.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Shared behaviour for the view-side content pack resolvers that map a set of stream references to native stream ids + * during installation. {@link WidgetEntity} and {@link SearchTypeEntity} implement the same contract, so their cases + * are parameterized over a {@link StreamResolver} per subject. {@code QueryEntity} resolves stream references inside + * filters rather than a stream set and is covered separately in {@code QueryEntityTest}. + */ +class StreamReferenceResolutionTest { + + /** + * Maps a subject's content pack stream references to the resolved native stream ids, or throws a + * {@link ContentPackException} when the entity map holds a non-{@link Stream} value for a reference. + */ + @FunctionalInterface + interface StreamResolver { + Set resolve(String subjectId, Set streamRefs, Map nativeEntities); + } + + static List subjects() { + return List.of( + Arguments.of("widget", (StreamResolver) StreamReferenceResolutionTest::resolveViaWidget), + Arguments.of("searchType", (StreamResolver) StreamReferenceResolutionTest::resolveViaSearchType)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("subjects") + void dropsUnresolvableStreamReferenceInsteadOfFailing(String subject, StreamResolver resolver) { + assertThat(resolver.resolve("subject-id", Set.of("missing-stream-id"), Collections.emptyMap())) + .isEmpty(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("subjects") + void keepsResolvableStreamReferenceAndDropsUnresolvable(String subject, StreamResolver resolver) { + assertThat(resolver.resolve("subject-id", Set.of("cp-stream-id", "missing-stream-id"), + nativeStream("cp-stream-id", "native-stream-id"))) + .containsExactly("native-stream-id"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("subjects") + void failsOnWrongTypeStreamReference(String subject, StreamResolver resolver) { + // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. + final Map nativeEntities = + Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); + + assertThatThrownBy(() -> resolver.resolve("subject-id", Set.of("cp-stream-id"), nativeEntities)) + .isInstanceOf(ContentPackException.class) + .hasMessageContaining("cp-stream-id") + .hasMessageContaining("subject-id"); + } + + private static Map nativeStream(String contentPackId, String nativeId) { + final Stream stream = mock(Stream.class); + when(stream.getId()).thenReturn(nativeId); + return Map.of(EntityDescriptor.create(contentPackId, ModelTypes.STREAM_REF_V1), stream); + } + + private static Set resolveViaWidget(String subjectId, Set streamRefs, + Map nativeEntities) { + final WidgetEntity widget = WidgetEntity.builder() + .id(subjectId) + .type(MessageListConfigDTO.NAME) + .filters(Collections.emptyList()) + .timerange(KeywordRange.create("last 5 minutes", "Etc/UTC")) + .query(ElasticsearchQueryString.of("*")) + .streams(streamRefs) + .config(MessageListConfigDTO.Builder.builder() + .fields(ImmutableSet.of()) + .showMessageRow(false) + .build()) + .build(); + return widget.toNativeEntity(Collections.emptyMap(), nativeEntities).streams(); + } + + private static Set resolveViaSearchType(String subjectId, Set streamRefs, + Map nativeEntities) { + final MessageListEntity searchType = MessageListEntity.builder() + .id(subjectId) + .streams(ImmutableSet.copyOf(streamRefs)) + .build(); + return searchType.toNativeEntity(Collections.emptyMap(), nativeEntities).streams(); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java b/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java deleted file mode 100644 index 8f18b8b47cce..000000000000 --- a/graylog2-server/src/test/java/org/graylog2/contentpacks/model/entities/WidgetEntityTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -package org.graylog2.contentpacks.model.entities; - -import com.google.common.collect.ImmutableSet; -import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; -import org.graylog.plugins.views.search.views.WidgetDTO; -import org.graylog.plugins.views.search.views.widgets.messagelist.MessageListConfigDTO; -import org.graylog2.contentpacks.exceptions.ContentPackException; -import org.graylog2.contentpacks.model.ModelTypes; -import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; -import org.graylog2.plugin.streams.Stream; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class WidgetEntityTest { - - private WidgetEntity.Builder widgetBuilder(Set streams) { - return WidgetEntity.builder() - .id("widget-id") - .type(MessageListConfigDTO.NAME) - .filters(Collections.emptyList()) - .timerange(KeywordRange.create("last 5 minutes", "Etc/UTC")) - .query(ElasticsearchQueryString.of("*")) - .streams(streams) - .config(MessageListConfigDTO.Builder.builder() - .fields(ImmutableSet.of()) - .showMessageRow(false) - .build()); - } - - @Test - public void dropsUnresolvableStreamReferenceInsteadOfFailing() { - final WidgetEntity widget = widgetBuilder(ImmutableSet.of("missing-stream-id")).build(); - - final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()); - - assertThat(nativeEntity.streams()).isEmpty(); - } - - @Test - public void keepsResolvableStreamReference() { - final Stream stream = mock(Stream.class); - when(stream.getId()).thenReturn("native-stream-id"); - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); - - final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id")).build(); - - final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), nativeEntities); - - assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); - } - - @Test - public void dropsOnlyUnresolvableStreamReferences() { - final Stream stream = mock(Stream.class); - when(stream.getId()).thenReturn("native-stream-id"); - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream); - - final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id", "missing-stream-id")).build(); - - final WidgetDTO nativeEntity = widget.toNativeEntity(Collections.emptyMap(), nativeEntities); - - assertThat(nativeEntity.streams()).containsExactly("native-stream-id"); - } - - @Test - public void failsOnWrongTypeStreamReference() { - // A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install. - final Map nativeEntities = - Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream"); - - final WidgetEntity widget = widgetBuilder(ImmutableSet.of("cp-stream-id")).build(); - - assertThatThrownBy(() -> widget.toNativeEntity(Collections.emptyMap(), nativeEntities)) - .isInstanceOf(ContentPackException.class) - .hasMessageContaining("cp-stream-id") - .hasMessageContaining("widget-id"); - } -}