mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
3868 lines
131 KiB
TypeScript
3868 lines
131 KiB
TypeScript
// Copyright 2026 OpenObserve Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// 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
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
|
|
import { mount, flushPromises } from "@vue/test-utils";
|
|
import i18n from "@/locales";
|
|
// @ts-ignore
|
|
import store from "@/test/unit/helpers/store";
|
|
import config from "@/aws-exports";
|
|
|
|
const {
|
|
mockToast,
|
|
mockGetStream,
|
|
mockStreamServiceSchema,
|
|
mockStreamServiceList,
|
|
mockUpdateSettings,
|
|
} = vi.hoisted(() => ({
|
|
mockToast: vi.fn(() => vi.fn()),
|
|
mockGetStream: vi.fn(),
|
|
mockStreamServiceSchema: vi.fn(),
|
|
mockStreamServiceList: vi.fn(),
|
|
mockUpdateSettings: vi.fn().mockResolvedValue({ data: { code: 200 } }),
|
|
}));
|
|
|
|
vi.mock("@/lib/feedback/Toast/useToast", () => ({
|
|
toast: mockToast,
|
|
useToast: () => ({ toast: mockToast, toasts: [] }),
|
|
toastRecords: [],
|
|
}));
|
|
|
|
vi.mock("@/composables/useStreams", () => ({
|
|
default: () => ({
|
|
getStream: mockGetStream,
|
|
getStreams: vi.fn().mockResolvedValue({}),
|
|
getUpdatedSettings: vi.fn((_prev, settings) => settings),
|
|
streamsCache: {
|
|
logs: { value: {} },
|
|
traces: { value: {} },
|
|
metrics: { value: {} },
|
|
enrichment_tables: { value: {} },
|
|
},
|
|
updateStreamsInStore: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/services/stream", async (importOriginal) => {
|
|
const { overlayServiceMock } = await import("@/test/unit/helpers/mockService");
|
|
return overlayServiceMock(await importOriginal(), {
|
|
default: {
|
|
schema: mockStreamServiceSchema,
|
|
list: mockStreamServiceList,
|
|
deleteFields: vi.fn().mockResolvedValue({ data: { code: 200 } }),
|
|
updateSettings: mockUpdateSettings,
|
|
nameList: vi.fn().mockResolvedValue({ data: [] }),
|
|
getStreamStats: vi.fn().mockResolvedValue({ data: {} }),
|
|
},
|
|
});
|
|
});
|
|
|
|
vi.mock("@/services/product_analytics", () => ({ default: { track: vi.fn() } }));
|
|
import analytics from "@/services/product_analytics";
|
|
|
|
import LogStream from "@/components/logstream/schema.vue";
|
|
|
|
// Lightweight stub for ODrawer so tests can render the slots schema.vue
|
|
// places inside the drawer (header badge, default content, footer) without
|
|
// pulling in reka-ui portals. Exposes the migrated prop surface and emits
|
|
// update:open + click:primary/secondary/neutral.
|
|
const ODrawerStub = {
|
|
name: "ODrawer",
|
|
props: {
|
|
open: { type: Boolean, default: false },
|
|
size: { type: String, default: undefined },
|
|
title: { type: String, default: undefined },
|
|
titleDataTest: { type: String, default: undefined },
|
|
subTitle: { type: String, default: undefined },
|
|
persistent: { type: Boolean, default: false },
|
|
showClose: { type: Boolean, default: true },
|
|
width: { type: [String, Number], default: undefined },
|
|
primaryButtonLabel: { type: String, default: undefined },
|
|
secondaryButtonLabel: { type: String, default: undefined },
|
|
neutralButtonLabel: { type: String, default: undefined },
|
|
primaryButtonVariant: { type: String, default: undefined },
|
|
secondaryButtonVariant: { type: String, default: undefined },
|
|
neutralButtonVariant: { type: String, default: undefined },
|
|
primaryButtonDisabled: { type: Boolean, default: false },
|
|
secondaryButtonDisabled: { type: Boolean, default: false },
|
|
neutralButtonDisabled: { type: Boolean, default: false },
|
|
primaryButtonLoading: { type: Boolean, default: false },
|
|
secondaryButtonLoading: { type: Boolean, default: false },
|
|
neutralButtonLoading: { type: Boolean, default: false },
|
|
},
|
|
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
|
|
template: `
|
|
<div
|
|
data-test="o-drawer-stub"
|
|
:data-open="String(open)"
|
|
:data-size="size"
|
|
:data-width="width"
|
|
:data-persistent="String(!!persistent)"
|
|
:data-show-close="String(!!showClose)"
|
|
>
|
|
<span v-if="title" :data-test="titleDataTest">{{ title }}</span>
|
|
<span v-if="subTitle" data-test="o-drawer-sub-title">{{ subTitle }}</span>
|
|
<slot name="header" />
|
|
<slot name="header-left" />
|
|
<slot name="header-right" />
|
|
<slot />
|
|
<slot name="footer" />
|
|
<button
|
|
data-test="o-drawer-stub-primary"
|
|
@click="$emit('click:primary')"
|
|
>primary</button>
|
|
<button
|
|
data-test="o-drawer-stub-secondary"
|
|
@click="$emit('click:secondary')"
|
|
>secondary</button>
|
|
<button
|
|
data-test="o-drawer-stub-close"
|
|
@click="$emit('update:open', false)"
|
|
>close</button>
|
|
</div>
|
|
`,
|
|
};
|
|
|
|
describe("Schema Component Tests", () => {
|
|
let wrapper: any;
|
|
|
|
beforeEach(() => {
|
|
// Default mock for getStream — resolves with minimal valid stream data.
|
|
// Individual test groups override this with more specific data as needed.
|
|
mockGetStream.mockResolvedValue({
|
|
name: "test-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
});
|
|
|
|
// Additional comprehensive tests for schema.vue functions from lines 697-1784
|
|
describe("Comprehensive Schema Function Tests", () => {
|
|
const originalIsEnterprise = (config as any).isEnterprise;
|
|
|
|
beforeEach(async () => {
|
|
// The SDR column/action is shown locked (not hidden) in builds that
|
|
// don't unlock it — these tests exercise the unlocked action itself,
|
|
// so they need an entitled build. `columns` reads this once at setup,
|
|
// so it must be set before mount, not inside an individual `it`.
|
|
(config as any).isEnterprise = "true";
|
|
mockGetStream.mockResolvedValue({
|
|
name: "test-stream",
|
|
storage_type: "s3",
|
|
stream_type: "logs",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 400,
|
|
file_num: 1,
|
|
storage_size: 0.74,
|
|
compressed_size: 0.03,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
// Reset wrapper for each test
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
storage_type: "s3",
|
|
stream_type: "logs",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 400,
|
|
file_num: 1,
|
|
storage_size: 0.74,
|
|
compressed_size: 0.03,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: {
|
|
store: store,
|
|
},
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
(config as any).isEnterprise = originalIsEnterprise;
|
|
});
|
|
|
|
// Test 1: Component initialization with default values
|
|
|
|
// Test 2: markFormDirty function
|
|
|
|
// Test 3: changePagination function
|
|
|
|
// Test 4: updateActiveTab function
|
|
|
|
// Test 5: updateActiveMainTab function
|
|
|
|
// Test 6: openDialog function
|
|
|
|
// Test 7: closeDialog function
|
|
|
|
// Test 8: hasUserDefinedSchema computed property
|
|
it("should compute hasUserDefinedSchema correctly", () => {
|
|
// Initially false
|
|
expect(wrapper.vm.hasUserDefinedSchema).toBe(false);
|
|
|
|
// Set defined schema fields
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1"];
|
|
expect(wrapper.vm.hasUserDefinedSchema).toBe(true);
|
|
});
|
|
|
|
// Test 10: showDataRetention computed property
|
|
it("should compute showDataRetention correctly", async () => {
|
|
expect(wrapper.vm.showDataRetention).toBe(true);
|
|
|
|
// Test with enrichment_tables
|
|
const enrichmentWrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
stream_type: "enrichment_tables",
|
|
settings: { defined_schema_fields: [] },
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store: store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
expect(enrichmentWrapper.vm.showDataRetention).toBe(false);
|
|
enrichmentWrapper.unmount();
|
|
});
|
|
|
|
// Test 12: getFieldIndices function with full text search
|
|
it("should get correct field indices for full text search", () => {
|
|
const property = { name: "message" };
|
|
const settings = {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
partition_keys: {},
|
|
};
|
|
|
|
// Mock store properties that are used in getFieldIndices
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys =
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys || [];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields =
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields || [];
|
|
|
|
const indices = wrapper.vm.getFieldIndices(property, settings);
|
|
expect(indices).toContain("fullTextSearchKey");
|
|
expect(property.index_type).toContain("fullTextSearchKey");
|
|
});
|
|
|
|
// Test 14: getFieldIndices function with secondary index
|
|
it("should get correct field indices for secondary index", () => {
|
|
const property = { name: "status" };
|
|
const settings = {
|
|
full_text_search_keys: [],
|
|
index_fields: ["status"],
|
|
bloom_filter_fields: [],
|
|
partition_keys: {},
|
|
};
|
|
|
|
// Mock store properties that are used in getFieldIndices
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys =
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys || [];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields =
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields || [];
|
|
|
|
const indices = wrapper.vm.getFieldIndices(property, settings);
|
|
expect(indices).toContain("secondaryIndexKey");
|
|
});
|
|
|
|
// Test 15: getFieldIndices function with bloom filter
|
|
it("should get correct field indices for bloom filter", () => {
|
|
const property = { name: "userId" };
|
|
const settings = {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: ["userId"],
|
|
partition_keys: {},
|
|
};
|
|
|
|
// Mock store properties that are used in getFieldIndices
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys =
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys || [];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields =
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields || [];
|
|
|
|
const indices = wrapper.vm.getFieldIndices(property, settings);
|
|
expect(indices).toContain("bloomFilterKey");
|
|
});
|
|
|
|
// Test 16: getFieldIndices function with key partition
|
|
it("should get correct field indices for key partition", () => {
|
|
const property = { name: "region" };
|
|
const settings = {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
partition_keys: {
|
|
level1: { field: "region", types: "value", disabled: false },
|
|
},
|
|
};
|
|
|
|
// Mock store properties that are used in getFieldIndices
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys =
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys || [];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields =
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields || [];
|
|
|
|
const indices = wrapper.vm.getFieldIndices(property, settings);
|
|
expect(indices).toContain("keyPartition");
|
|
expect(property.level).toBe("level1");
|
|
});
|
|
|
|
// Test 17: getFieldIndices function with hash partition
|
|
it("should get correct field indices for hash partition", () => {
|
|
const property = { name: "customerId" };
|
|
const settings = {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
partition_keys: {
|
|
level1: { field: "customerId", types: { hash: 16 }, disabled: false },
|
|
},
|
|
};
|
|
|
|
// Mock store properties that are used in getFieldIndices
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys =
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys || [];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields =
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields || [];
|
|
|
|
const indices = wrapper.vm.getFieldIndices(property, settings);
|
|
expect(indices).toContain("hashPartition_16");
|
|
});
|
|
|
|
// Test 18: updateDefinedSchemaFields function - removing fields
|
|
it("should update defined schema fields when removing", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1", "field2", "field3"];
|
|
wrapper.vm.selectedFields = [{ name: "field2" }];
|
|
wrapper.vm.activeTab = "schemaFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toEqual(["field1", "field3"]);
|
|
expect(wrapper.vm.selectedFields).toEqual([]);
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
// Test 19: updateDefinedSchemaFields function - adding fields
|
|
it("should update defined schema fields when adding", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1"];
|
|
wrapper.vm.selectedFields = [{ name: "field2" }];
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("field1");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("field2");
|
|
});
|
|
|
|
// Test 20: updateDefinedSchemaFields function - switch tab when empty
|
|
it("should switch to allFields when schema fields becomes empty", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1"];
|
|
wrapper.vm.selectedFields = [{ name: "field1" }];
|
|
wrapper.vm.activeTab = "schemaFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toEqual([]);
|
|
expect(wrapper.vm.activeTab).toBe("allFields");
|
|
});
|
|
|
|
// Test 21: updateStreamResponse function
|
|
it("should update stream response with user defined flags", () => {
|
|
const streamResponse = {
|
|
settings: {
|
|
defined_schema_fields: ["field1"],
|
|
},
|
|
schema: [
|
|
{ name: "field1", type: "string" },
|
|
{ name: "field2", type: "number" },
|
|
],
|
|
};
|
|
|
|
const result = wrapper.vm.updateStreamResponse(streamResponse);
|
|
|
|
expect(result.schema[0].isUserDefined).toBe(true);
|
|
expect(result.schema[1].isUserDefined).toBe(false);
|
|
});
|
|
|
|
// Test 22: formatDate function
|
|
it("should format date correctly", () => {
|
|
const result = wrapper.vm.formatDate("2023-12-25T10:30:00");
|
|
expect(result).toBe("25-12-2023");
|
|
});
|
|
|
|
// Test 23: convertUnixToDateFormat function
|
|
it("should convert unix timestamp correctly", () => {
|
|
const unixMicroseconds = 1703505000000000; // 2023-12-25
|
|
const result = wrapper.vm.convertUnixToDateFormat(unixMicroseconds);
|
|
expect(result).toMatch(/\d{2}-\d{2}-\d{4}/);
|
|
});
|
|
|
|
// Test 24: convertUnixToDateFormat with empty input
|
|
it("should handle empty unix timestamp", () => {
|
|
const result = wrapper.vm.convertUnixToDateFormat("");
|
|
expect(result).toBe("");
|
|
});
|
|
|
|
// Test 25: calculateDateRange function
|
|
it("should calculate date range correctly", () => {
|
|
wrapper.vm.dataRetentionDays = 30;
|
|
wrapper.vm.calculateDateRange();
|
|
expect(wrapper.vm.minDate).toBeDefined();
|
|
});
|
|
|
|
// Test 25: calculateDateRange function
|
|
it("should leave minDate null when data retention is unset (0)", () => {
|
|
wrapper.vm.dataRetentionDays = 0;
|
|
wrapper.vm.calculateDateRange();
|
|
expect(wrapper.vm.minDate).toBeNull();
|
|
});
|
|
|
|
// Test 26: openPatternAssociationDialog function
|
|
it("should open pattern association dialog", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
field1: [{ field: "field1", pattern_name: "pattern1", pattern_id: "id1" }],
|
|
};
|
|
|
|
wrapper.vm.openPatternAssociationDialog("field1");
|
|
|
|
expect(wrapper.vm.patternAssociationDialog.show).toBe(true);
|
|
expect(wrapper.vm.patternAssociationDialog.fieldName).toBe("field1");
|
|
});
|
|
|
|
// Test 27: handleAddPattern function
|
|
it("should add new pattern correctly", () => {
|
|
const pattern = {
|
|
field: "field1",
|
|
pattern_name: "test-pattern",
|
|
pattern_id: "pattern-123",
|
|
policy: "replace",
|
|
apply_at: "ingestion",
|
|
};
|
|
|
|
wrapper.vm.handleAddPattern(pattern);
|
|
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
expect(wrapper.vm.patternAssociations.field1).toEqual(
|
|
expect.arrayContaining([expect.objectContaining(pattern)]),
|
|
);
|
|
});
|
|
|
|
// Test 28: handleRemovePattern function
|
|
it("should remove pattern correctly", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
field1: [
|
|
{ field: "field1", pattern_name: "pattern1", pattern_id: "id1" },
|
|
{ field: "field1", pattern_name: "pattern2", pattern_id: "id2" },
|
|
],
|
|
};
|
|
|
|
wrapper.vm.handleRemovePattern("id1", "field1");
|
|
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
expect(wrapper.vm.patternAssociations.field1).toHaveLength(1);
|
|
expect(wrapper.vm.patternAssociations.field1[0].pattern_id).toBe("id2");
|
|
});
|
|
|
|
// Test 29: handleUpdateAppliedPattern for policy
|
|
it("should update applied pattern policy", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
field1: [{ field: "field1", pattern_name: "pattern1", pattern_id: "id1", policy: "old" }],
|
|
};
|
|
|
|
const updatedPattern = {
|
|
field: "field1",
|
|
pattern_name: "pattern1",
|
|
pattern_id: "id1",
|
|
policy: "new",
|
|
};
|
|
|
|
wrapper.vm.handleUpdateAppliedPattern(updatedPattern, "field1", "id1", "policy");
|
|
expect(wrapper.vm.patternAssociations.field1[0].policy).toBe("new");
|
|
});
|
|
|
|
// Test 30: scrollToAddFields function
|
|
|
|
// Test 31: dateChangeValue function
|
|
// NOTE: dateChangeValue internally calls convertDateToTimestamp which uses
|
|
// Luxon's DateTime.fromObject. That call can fail in jsdom environments that
|
|
// lack full Intl support. When the timestamp conversion fails, both
|
|
// startTimestamp and endTimestamp are 0, so the push to redDaysList is
|
|
// skipped and onSubmit is never called. The formatDate sub-function is
|
|
// tested separately above. This test verifies the function runs without
|
|
// throwing and handles the relativeTimePeriod path correctly.
|
|
it("should handle date change with relative time period", () => {
|
|
const dateValue = { relativeTimePeriod: "1h" };
|
|
|
|
wrapper.vm.dateChangeValue(dateValue);
|
|
// When relativeTimePeriod is set, the function should exit early without
|
|
// modifying redDaysList or calling onSubmit.
|
|
expect(wrapper.vm.redDaysList).toEqual([]);
|
|
});
|
|
|
|
// Test 32: deleteDates function
|
|
it("should delete dates correctly", () => {
|
|
wrapper.vm.selectedDateFields = [
|
|
{ original_start: 1703505000000, original_end: 1703591400000 },
|
|
];
|
|
|
|
wrapper.vm.deleteDates();
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
// Test 33: groupPatternAssociationsByField function
|
|
it("should group pattern associations by field", () => {
|
|
const associations = [
|
|
{ field: "field1", pattern_name: "pattern1", pattern_id: "id1" },
|
|
{ field: "field1", pattern_name: "pattern2", pattern_id: "id2" },
|
|
{ field: "field2", pattern_name: "pattern3", pattern_id: "id3" },
|
|
];
|
|
|
|
const result = wrapper.vm.groupPatternAssociationsByField(associations);
|
|
expect(result.field1).toHaveLength(2);
|
|
expect(result.field2).toHaveLength(1);
|
|
});
|
|
|
|
// Test 34: ungroupPatternAssociations function
|
|
it("should ungroup pattern associations", () => {
|
|
const grouped = {
|
|
field1: [
|
|
{ field: "field1", pattern_name: "pattern1", pattern_id: "id1" },
|
|
{ field: "field1", pattern_name: "pattern2", pattern_id: "id2" },
|
|
],
|
|
field2: [{ field: "field2", pattern_name: "pattern3", pattern_id: "id3" }],
|
|
};
|
|
|
|
const result = wrapper.vm.ungroupPatternAssociations(grouped);
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
|
|
// Test 35: computedSchemaFieldsName computed property
|
|
|
|
// Test 36: tabs computed property
|
|
|
|
// Test 37: mainTabs computed property
|
|
|
|
// Test 38: isSchemaUDSEnabled computed property
|
|
|
|
// Test 39: allFieldsName computed property
|
|
|
|
// Test 40: showStoreOriginalDataToggle computed property
|
|
it("should compute store original data toggle visibility", async () => {
|
|
expect(wrapper.vm.showStoreOriginalDataToggle).toBe(true);
|
|
|
|
for (const streamType of ["traces", "metrics", "profiles", "metadata", "enrichment_tables"]) {
|
|
const otherWrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
stream_type: streamType,
|
|
settings: { defined_schema_fields: [] },
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store: store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
expect(otherWrapper.vm.showStoreOriginalDataToggle).toBe(false);
|
|
otherWrapper.unmount();
|
|
}
|
|
});
|
|
|
|
// Test 41-50: Additional edge cases and error scenarios
|
|
it("should handle empty pattern associations in openPatternAssociationDialog", () => {
|
|
wrapper.vm.openPatternAssociationDialog("nonexistent");
|
|
expect(wrapper.vm.patternAssociationDialog.data).toEqual([]);
|
|
});
|
|
|
|
it("should handle adding pattern to existing field", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
field1: [{ field: "field1", pattern_name: "existing", pattern_id: "existing-id" }],
|
|
};
|
|
|
|
const newPattern = {
|
|
field: "field1",
|
|
pattern_name: "new-pattern",
|
|
pattern_id: "new-id",
|
|
};
|
|
|
|
wrapper.vm.handleAddPattern(newPattern);
|
|
expect(wrapper.vm.patternAssociations.field1).toHaveLength(2);
|
|
});
|
|
|
|
it("should handle updateAppliedPattern with apply_at attribute", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
field1: [{ field: "field1", pattern_name: "pattern1", pattern_id: "id1", apply_at: "old" }],
|
|
};
|
|
|
|
const updatedPattern = {
|
|
field: "field1",
|
|
pattern_name: "pattern1",
|
|
pattern_id: "id1",
|
|
apply_at: "new",
|
|
};
|
|
|
|
wrapper.vm.handleUpdateAppliedPattern(updatedPattern, "field1", "id1", "apply_at");
|
|
expect(wrapper.vm.patternAssociations.field1[0].apply_at).toBe("new");
|
|
});
|
|
|
|
it("should handle filter field function with schema fields", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1", "field2"];
|
|
|
|
const rows = [{ name: "field1" }, { name: "field2" }, { name: "field3" }];
|
|
|
|
const result = wrapper.vm.filterFieldFn(rows, "field@schemaFields");
|
|
expect(result.length).toBe(2);
|
|
});
|
|
|
|
it("should handle empty date value in dateChangeValue", () => {
|
|
const dateValue = { relativeTimePeriod: "1h" };
|
|
wrapper.vm.dateChangeValue(dateValue);
|
|
// Should not add to redDaysList
|
|
expect(wrapper.vm.redDaysList).toEqual([]);
|
|
});
|
|
|
|
it("should handle empty stream response in updateStreamResponse", () => {
|
|
const streamResponse = {
|
|
settings: {},
|
|
schema: [],
|
|
};
|
|
|
|
const result = wrapper.vm.updateStreamResponse(streamResponse);
|
|
expect(result.schema).toEqual([]);
|
|
});
|
|
|
|
it("should handle partition key conflicts in disableOptions", () => {
|
|
const schema = { index_type: ["prefixPartition"] };
|
|
const keyPartitionOption = { value: "keyPartition" };
|
|
|
|
expect(wrapper.vm.disableOptions(schema, keyPartitionOption)).toBe(true);
|
|
});
|
|
|
|
it("should handle hash partition conflicts in disableOptions", () => {
|
|
const schema = { index_type: ["hashPartition_8"] };
|
|
const hashOption = { value: "hashPartition_16" };
|
|
|
|
expect(wrapper.vm.disableOptions(schema, hashOption)).toBe(true);
|
|
});
|
|
|
|
it("should not disable non-conflicting options", () => {
|
|
const schema = { index_type: ["fullTextSearchKey"] };
|
|
const bloomOption = { value: "bloomFilterKey" };
|
|
|
|
expect(wrapper.vm.disableOptions(schema, bloomOption)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Performance Fields Popup Tests
|
|
describe("Performance Fields Popup Tests", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "test-stream",
|
|
storage_type: "s3",
|
|
stream_type: "logs",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 400,
|
|
file_num: 1,
|
|
storage_size: 0.74,
|
|
compressed_size: 0.03,
|
|
},
|
|
schema: [
|
|
{ name: "user_id", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "log_level", type: "Utf8", index_type: ["secondaryIndexKey"] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "timestamp", type: "Int64", index_type: [] },
|
|
],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: ["user_id", "message"],
|
|
index_fields: ["log_level"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
storage_type: "s3",
|
|
stream_type: "logs",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 400,
|
|
file_num: 1,
|
|
storage_size: 0.74,
|
|
compressed_size: 0.03,
|
|
},
|
|
schema: [
|
|
{
|
|
name: "user_id",
|
|
type: "Utf8",
|
|
index_type: ["fullTextSearchKey"],
|
|
},
|
|
{
|
|
name: "log_level",
|
|
type: "Utf8",
|
|
index_type: ["secondaryIndexKey"],
|
|
},
|
|
{
|
|
name: "message",
|
|
type: "Utf8",
|
|
index_type: ["fullTextSearchKey"],
|
|
},
|
|
{
|
|
name: "timestamp",
|
|
type: "Int64",
|
|
index_type: [],
|
|
},
|
|
],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: ["user_id", "message"],
|
|
index_fields: ["log_level"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: {
|
|
store: {
|
|
...store,
|
|
state: {
|
|
...store.state,
|
|
zoConfig: {
|
|
...store.state.zoConfig,
|
|
default_fts_keys: ["user_id"],
|
|
default_secondary_index_fields: ["log_level"],
|
|
user_defined_schema_max_fields: 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
// Test 1: getMissingPerformanceFields should detect missing FTS fields
|
|
it("should detect missing FTS fields", () => {
|
|
const selectedFieldsSet = new Set(["timestamp"]); // Not selecting FTS fields
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
expect(missing.length).toBeGreaterThan(0);
|
|
const ftsFields = missing.filter((f) => f.type === "Full Text Search");
|
|
expect(ftsFields.length).toBeGreaterThan(0);
|
|
expect(ftsFields.some((f) => f.name === "user_id" || f.name === "message")).toBe(true);
|
|
});
|
|
|
|
// Test 2: getMissingPerformanceFields should detect missing Secondary Index fields
|
|
it("should detect missing Secondary Index fields", () => {
|
|
const selectedFieldsSet = new Set(["timestamp"]); // Not selecting secondary index fields
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
expect(missing.length).toBeGreaterThan(0);
|
|
const secondaryIndexFields = missing.filter((f) => f.type === "Secondary Index");
|
|
expect(secondaryIndexFields.length).toBeGreaterThan(0);
|
|
expect(secondaryIndexFields.some((f) => f.name === "log_level")).toBe(true);
|
|
});
|
|
|
|
// Test 3: getMissingPerformanceFields should not detect fields already selected
|
|
it("should not detect fields already selected by user", () => {
|
|
const selectedFieldsSet = new Set(["user_id", "message", "log_level", "timestamp"]);
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
expect(missing.length).toBe(0);
|
|
});
|
|
|
|
// Test 4: getMissingPerformanceFields should detect default FTS keys from config
|
|
it("should detect default FTS keys from backend config", () => {
|
|
// user_id is in default_fts_keys
|
|
const selectedFieldsSet = new Set(["message", "timestamp"]); // Not including user_id
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
const ftsFields = missing.filter((f) => f.type === "Full Text Search");
|
|
expect(ftsFields.some((f) => f.name === "user_id")).toBe(true);
|
|
});
|
|
|
|
// Test 5: getMissingPerformanceFields should detect default secondary index keys from config
|
|
it("should detect default secondary index keys from backend config", () => {
|
|
// log_level is in default_secondary_index_fields
|
|
const selectedFieldsSet = new Set(["user_id", "timestamp"]); // Not including log_level
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
const secondaryIndexFields = missing.filter((f) => f.type === "Secondary Index");
|
|
expect(secondaryIndexFields.some((f) => f.name === "log_level")).toBe(true);
|
|
});
|
|
|
|
// Test 6: updateDefinedSchemaFields should show popup when UDS is enabled first time with missing fields
|
|
it("should show performance fields popup when enabling UDS for first time", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = []; // First time enabling UDS
|
|
wrapper.vm.selectedFields = [{ name: "timestamp" }];
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(true);
|
|
expect(wrapper.vm.missingPerformanceFields.length).toBeGreaterThan(0);
|
|
expect(wrapper.vm.pendingSelectedFields).toEqual(["timestamp"]);
|
|
});
|
|
|
|
// Test 7: updateDefinedSchemaFields should not show popup when all performance fields are selected
|
|
it("should not show popup when all performance fields are already selected", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = []; // First time enabling UDS
|
|
wrapper.vm.selectedFields = [
|
|
{ name: "user_id" },
|
|
{ name: "message" },
|
|
{ name: "log_level" },
|
|
{ name: "timestamp" },
|
|
];
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("user_id");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("message");
|
|
});
|
|
|
|
// Test 8: updateDefinedSchemaFields should not show popup when UDS already has fields
|
|
it("should not show popup when UDS already has existing fields", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["existing_field"]; // Already has fields
|
|
wrapper.vm.selectedFields = [{ name: "timestamp" }];
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("existing_field");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("timestamp");
|
|
});
|
|
|
|
// Test 9: addPerformanceFields should add missing fields when user clicks OK
|
|
it("should add missing performance fields when user clicks OK", () => {
|
|
wrapper.vm.pendingSelectedFields = ["timestamp"];
|
|
wrapper.vm.missingPerformanceFields = [
|
|
{ name: "user_id", type: "Full Text Search" },
|
|
{ name: "log_level", type: "Secondary Index" },
|
|
];
|
|
|
|
wrapper.vm.addPerformanceFields();
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("timestamp");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("user_id");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("log_level");
|
|
expect(wrapper.vm.missingPerformanceFields).toEqual([]);
|
|
expect(wrapper.vm.pendingSelectedFields).toEqual([]);
|
|
});
|
|
|
|
// Test 10: skipPerformanceFields should not add missing fields when user clicks Skip
|
|
it("should not add missing performance fields when user clicks Skip", () => {
|
|
wrapper.vm.pendingSelectedFields = ["timestamp"];
|
|
wrapper.vm.missingPerformanceFields = [
|
|
{ name: "user_id", type: "Full Text Search" },
|
|
{ name: "log_level", type: "Secondary Index" },
|
|
];
|
|
|
|
wrapper.vm.skipPerformanceFields();
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("timestamp");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("user_id");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("log_level");
|
|
expect(wrapper.vm.missingPerformanceFields).toEqual([]);
|
|
expect(wrapper.vm.pendingSelectedFields).toEqual([]);
|
|
});
|
|
|
|
// Test 11: Removed test for missingPerformanceFieldsByType
|
|
// This computed property is now internal to PerformanceFieldsDialog component
|
|
// The grouping functionality is still tested through the dialog component's behavior
|
|
|
|
// Test 12: proceedWithAddingFields should filter out timestamp and allFields
|
|
it("should filter out timestamp and _all fields when adding", () => {
|
|
const selectedFieldsSet = new Set(["user_id", "_all", "_timestamp"]);
|
|
wrapper.vm.store.state.zoConfig.timestamp_column = "_timestamp";
|
|
|
|
wrapper.vm.proceedWithAddingFields(selectedFieldsSet);
|
|
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("user_id");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("_all");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("_timestamp");
|
|
});
|
|
|
|
// Test 13: proceedWithAddingFields should mark form as dirty
|
|
it("should mark form as dirty when adding fields", () => {
|
|
wrapper.vm.formDirtyFlag = false;
|
|
const selectedFieldsSet = new Set(["user_id"]);
|
|
|
|
wrapper.vm.proceedWithAddingFields(selectedFieldsSet);
|
|
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
// Test 14: getMissingPerformanceFields should only include fields that exist in schema
|
|
it("should only include backend default fields that exist in current schema", () => {
|
|
// Add a field to default config that doesn't exist in schema
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys = ["user_id", "nonexistent_field"];
|
|
|
|
const selectedFieldsSet = new Set(["timestamp"]);
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
const ftsFields = missing.filter((f) => f.type === "Full Text Search");
|
|
expect(ftsFields.some((f) => f.name === "user_id")).toBe(true);
|
|
expect(ftsFields.some((f) => f.name === "nonexistent_field")).toBe(false);
|
|
});
|
|
|
|
// Test 15: updateDefinedSchemaFields should respect max field limit
|
|
it("should show error when exceeding max field limit", () => {
|
|
mockToast.mockClear();
|
|
wrapper.vm.store.state.zoConfig.user_defined_schema_max_fields = 5;
|
|
wrapper.vm.indexData.defined_schema_fields = ["f1", "f2", "f3", "f4"];
|
|
wrapper.vm.selectedFields = [{ name: "f5" }, { name: "f6" }]; // Would exceed limit
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
expect(mockToast).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
variant: "error",
|
|
message: expect.stringContaining("Maximum allowed fields"),
|
|
}),
|
|
);
|
|
expect(wrapper.vm.selectedFields).toEqual([]);
|
|
});
|
|
|
|
// Test 16: updateActiveMainTab for new tabs (Configuration, Cross-Linking)
|
|
});
|
|
|
|
// Tests for new features added in Mar 11 update
|
|
describe("Stats Grid Tiles (added Mar 11)", () => {
|
|
let w: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "test-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 1000,
|
|
file_num: 5,
|
|
storage_size: 2.5,
|
|
compressed_size: 0.5,
|
|
index_size: 0.1,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 1000,
|
|
file_num: 5,
|
|
storage_size: 2.5,
|
|
compressed_size: 0.5,
|
|
index_size: 0.1,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w.unmount();
|
|
});
|
|
|
|
it("should render storage-size-tile", async () => {
|
|
expect(w.find('[data-test="storage-size-tile"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("formatSizeFromMB should format MB values with units", () => {
|
|
// 1024 MB = 1 GB
|
|
const result = w.vm.formatSizeFromMB(1024);
|
|
expect(typeof result).toBe("string");
|
|
expect(result.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("formatSizeFromMB should handle zero", () => {
|
|
const result = w.vm.formatSizeFromMB(0);
|
|
expect(typeof result).toBe("string");
|
|
});
|
|
|
|
it("formatSizeFromMB should handle small values", () => {
|
|
const result = w.vm.formatSizeFromMB(0.5);
|
|
expect(typeof result).toBe("string");
|
|
});
|
|
});
|
|
|
|
describe("Timeline and Name Badge (added Mar 11)", () => {
|
|
let w: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "my-stream-name",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1000000,
|
|
doc_time_max: 2000000,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.2,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "my-stream-name",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1000000,
|
|
doc_time_max: 2000000,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.2,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w.unmount();
|
|
});
|
|
|
|
it("should show the stream name in the header badge", async () => {
|
|
expect(w.find('[data-test="schema-title-text"]').text()).toContain("my-stream-name");
|
|
});
|
|
});
|
|
|
|
describe("Cross-Linking Tab (added Mar 11)", () => {
|
|
it("should not render cross-linking tab when enable_cross_linking is false", async () => {
|
|
store.state.zoConfig = {
|
|
...store.state.zoConfig,
|
|
enable_cross_linking: false,
|
|
};
|
|
|
|
const w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "test-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: { provide: { store }, plugins: [i18n], stubs: { ODrawer: ODrawerStub } },
|
|
});
|
|
await flushPromises();
|
|
// When enable_cross_linking is false, the tab should not exist
|
|
expect(w.exists()).toBe(true);
|
|
w.unmount();
|
|
});
|
|
});
|
|
|
|
// Coverage for the ODrawer migration.
|
|
describe("ODrawer Migration", () => {
|
|
const buildProps = (open = true) => ({
|
|
open,
|
|
modelValue: {
|
|
name: "test-stream",
|
|
storage_type: "s3",
|
|
stream_type: "logs",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 400,
|
|
file_num: 1,
|
|
storage_size: 0.74,
|
|
compressed_size: 0.03,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
let w: any;
|
|
const originalIsEnterprise = (config as any).isEnterprise;
|
|
|
|
beforeEach(async () => {
|
|
// The pattern-association drawer's trigger is shown locked (not
|
|
// hidden) in builds that don't unlock it — `columns` reads this once
|
|
// at setup, so it must be set before mount, not inside an individual
|
|
// `it`. These tests exercise opening the drawer itself.
|
|
(config as any).isEnterprise = "true";
|
|
w = mount(LogStream, {
|
|
props: buildProps(true),
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w?.unmount();
|
|
vi.clearAllMocks();
|
|
(config as any).isEnterprise = originalIsEnterprise;
|
|
});
|
|
|
|
it("should emit update:open=false when the schema ODrawer emits update:open=false", async () => {
|
|
const drawers = w.findAllComponents(ODrawerStub);
|
|
await drawers[0].vm.$emit("update:open", false);
|
|
const emitted = w.emitted("update:open");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted[emitted.length - 1]).toEqual([false]);
|
|
});
|
|
|
|
it("should emit update:open=false when the ODrawer close button is clicked", async () => {
|
|
const drawers = w.findAllComponents(ODrawerStub);
|
|
await drawers[0].find('[data-test="o-drawer-stub-close"]').trigger("click");
|
|
await flushPromises();
|
|
const emitted = w.emitted("update:open");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted![emitted!.length - 1]).toEqual([false]);
|
|
});
|
|
|
|
it("should open the pattern association ODrawer when openPatternAssociationDialog is invoked", async () => {
|
|
w.vm.patternAssociations = {
|
|
field1: [{ field: "field1", pattern_name: "p1", pattern_id: "id1" }],
|
|
};
|
|
w.vm.openPatternAssociationDialog("field1");
|
|
await flushPromises();
|
|
const drawers = w.findAllComponents(ODrawerStub);
|
|
expect(drawers[1].props("open")).toBe(true);
|
|
});
|
|
|
|
it("should close the pattern association ODrawer when it emits update:open=false", async () => {
|
|
w.vm.patternAssociationDialog.show = true;
|
|
await flushPromises();
|
|
const drawers = w.findAllComponents(ODrawerStub);
|
|
expect(drawers[1].props("open")).toBe(true);
|
|
|
|
await drawers[1].vm.$emit("update:open", false);
|
|
await flushPromises();
|
|
expect(w.vm.patternAssociationDialog.show).toBe(false);
|
|
});
|
|
|
|
it("should hide schema content and show loading placeholder when indexData.schema is falsy", async () => {
|
|
// Override the default mock to return null schema so the loading
|
|
// placeholder is rendered instead of the full schema content.
|
|
mockGetStream.mockResolvedValue({
|
|
name: "loading-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
schema: null,
|
|
stats: {},
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
const lw = mount(LogStream, {
|
|
props: {
|
|
open: true,
|
|
modelValue: { name: "loading-stream", schema: null, stats: {}, settings: {} },
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
const drawer = lw.findComponent(ODrawerStub);
|
|
expect(drawer.exists()).toBe(true);
|
|
expect(drawer.text()).toContain("Wait while loading");
|
|
lw.unmount();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Configuration Tab
|
|
// ==========================================
|
|
describe("Configuration Tab", () => {
|
|
let w: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "config-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 500,
|
|
file_num: 3,
|
|
storage_size: 1.5,
|
|
compressed_size: 0.2,
|
|
index_size: 0.05,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
partition_time_level: "hourly",
|
|
partition_keys: {},
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 24,
|
|
flatten_level: null,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
enable_distinct_fields: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "config-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 500,
|
|
file_num: 3,
|
|
storage_size: 1.5,
|
|
compressed_size: 0.2,
|
|
index_size: 0.05,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 24,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w.unmount();
|
|
});
|
|
|
|
it("should update dataRetentionDays and mark form dirty", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
// OInput is a custom component; drive v-model through vm state
|
|
w.vm.dataRetentionDays = 90;
|
|
w.vm.markFormDirty();
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.dataRetentionDays).toBe(90);
|
|
expect(w.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
it("should update maxQueryRange and mark form dirty", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
w.vm.maxQueryRange = 48;
|
|
w.vm.markFormDirty();
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.maxQueryRange).toBe(48);
|
|
expect(w.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
it("should update flattenLevel and mark form dirty", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
w.vm.flattenLevel = 5;
|
|
w.vm.markFormDirty();
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.flattenLevel).toBe(5);
|
|
expect(w.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
it("should show the approxPartition switch and toggle it", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
const toggleBtn = w.find('[data-test="log-stream-use_approx-toggle-btn"]');
|
|
expect(toggleBtn.exists()).toBe(true);
|
|
|
|
// Toggle from false to true
|
|
w.vm.approxPartition = true;
|
|
w.vm.formDirtyFlag = true;
|
|
await w.vm.$nextTick();
|
|
expect(w.vm.approxPartition).toBe(true);
|
|
expect(w.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
it("should show storeOriginalData switch and toggle it", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
const toggleBtn = w.find('[data-test="log-stream-store-original-data-toggle-btn"]');
|
|
expect(toggleBtn.exists()).toBe(true);
|
|
|
|
w.vm.storeOriginalData = true;
|
|
w.vm.formDirtyFlag = true;
|
|
await w.vm.$nextTick();
|
|
expect(w.vm.storeOriginalData).toBe(true);
|
|
});
|
|
|
|
it("should show enableDistinctFields switch and toggle it", async () => {
|
|
w.vm.updateActiveMainTab("configuration");
|
|
await w.vm.$nextTick();
|
|
|
|
const toggleBtn = w.find('[data-test="log-stream-enabled-distinct-values-toggle-btn"]');
|
|
expect(toggleBtn.exists()).toBe(true);
|
|
|
|
w.vm.enableDistinctFields = true;
|
|
w.vm.formDirtyFlag = true;
|
|
await w.vm.$nextTick();
|
|
expect(w.vm.enableDistinctFields).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Red Button Tab
|
|
// ==========================================
|
|
describe("Red Button Tab (Extended Retention)", () => {
|
|
let w: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "retention-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 0.5,
|
|
compressed_size: 0.05,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [{ start: 1703505000, end: 1703591400 }],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "retention-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 0.5,
|
|
compressed_size: 0.05,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [{ start: 1703505000, end: 1703591400 }],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w.unmount();
|
|
});
|
|
|
|
it("should populate redBtnRows from extended_retention_days settings", () => {
|
|
expect(w.vm.redBtnRows.length).toBe(1);
|
|
expect(w.vm.redBtnRows[0].original_start).toBe(1703505000);
|
|
expect(w.vm.redBtnRows[0].original_end).toBe(1703591400);
|
|
});
|
|
|
|
it("should handle date selection callback", () => {
|
|
// dateChangeValue with relative time period exits early
|
|
w.vm.dateChangeValue({ relativeTimePeriod: "1h" });
|
|
expect(w.vm.redDaysList).toEqual([]);
|
|
});
|
|
|
|
it("should ignore programmatic mount-replay date-change emits (userChangedValue false)", () => {
|
|
// On mount/remount <date-time> emits an absolute range with
|
|
// userChangedValue:false. dateChangeValue must ignore it so it does not
|
|
// push to redDaysList and trigger onSubmit — the cause of the infinite
|
|
// updateSettings/getStream loop on the Extended Retention tab.
|
|
mockUpdateSettings.mockClear();
|
|
w.vm.dateChangeValue({
|
|
relativeTimePeriod: null,
|
|
userChangedValue: false,
|
|
selectedDate: { from: "2024-06-15", to: "2024-06-16" },
|
|
});
|
|
expect(w.vm.redDaysList).toEqual([]);
|
|
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should set IsdeleteBtnVisible when rows are selected", async () => {
|
|
w.vm.updateActiveMainTab("redButton");
|
|
await w.vm.$nextTick();
|
|
|
|
w.vm.selectedDateFields = [{ index: 0, start: "02-01-2024", end: "03-01-2024" }];
|
|
w.vm.IsdeleteBtnVisible = true;
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.IsdeleteBtnVisible).toBe(true);
|
|
});
|
|
|
|
it("should delete dates when deleteDates is called", async () => {
|
|
w.vm.updateActiveMainTab("redButton");
|
|
await w.vm.$nextTick();
|
|
|
|
w.vm.selectedDateFields = [
|
|
{ original_start: 1703505000, original_end: 1703591400, index: 0 },
|
|
];
|
|
// deleteDates sets formDirtyFlag then calls onSubmit()
|
|
// onSubmit() resets formDirtyFlag to false on success
|
|
await w.vm.deleteDates();
|
|
await flushPromises();
|
|
|
|
// After onSubmit completes successfully, the form should not be dirty
|
|
// because the component resets it after saving
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
expect(w.vm.loadingState).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Additional Functions Coverage
|
|
// ==========================================
|
|
describe("Additional Functions", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "funcs-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 200,
|
|
file_num: 2,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.1,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "job", type: "Utf8", index_type: [] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "funcs-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 200,
|
|
file_num: 2,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.1,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "job", type: "Utf8", index_type: [] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should enforce max index types (cap at 2)", () => {
|
|
const result = wrapper.vm.enforceMaxIndexTypes([
|
|
"fullTextSearchKey",
|
|
"secondaryIndexKey",
|
|
"bloomFilterKey",
|
|
]);
|
|
expect(result).toEqual(["secondaryIndexKey", "bloomFilterKey"]);
|
|
});
|
|
|
|
it("should enforce max index types (no cap when 2 or fewer)", () => {
|
|
const result = wrapper.vm.enforceMaxIndexTypes(["fullTextSearchKey", "bloomFilterKey"]);
|
|
expect(result).toEqual(["fullTextSearchKey", "bloomFilterKey"]);
|
|
});
|
|
|
|
it("should enforce max index types (return non-array as-is)", () => {
|
|
expect(wrapper.vm.enforceMaxIndexTypes("not-array")).toBe("not-array");
|
|
expect(wrapper.vm.enforceMaxIndexTypes(null)).toBe(null);
|
|
});
|
|
|
|
it("should detect env quick mode field", () => {
|
|
wrapper.vm.store.state.zoConfig.default_quick_mode_fields = ["job", "log"];
|
|
expect(wrapper.vm.isEnvQuickModeField("job")).toBe(true);
|
|
expect(wrapper.vm.isEnvQuickModeField("message")).toBe(false);
|
|
expect(wrapper.vm.isEnvQuickModeField("unknown")).toBe(false);
|
|
});
|
|
|
|
it("should compute quickModeIcon based on theme", () => {
|
|
expect(typeof wrapper.vm.quickModeIcon).toBe("string");
|
|
expect(wrapper.vm.quickModeIcon.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should compute getConfigIcon based on theme", () => {
|
|
expect(typeof wrapper.vm.getConfigIcon).toBe("string");
|
|
expect(wrapper.vm.getConfigIcon.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should add and remove schema fields via the form", async () => {
|
|
// Rows are owned by the child (StreamFieldInputs) through the form now —
|
|
// add/remove go through form.pushFieldValue / form.removeFieldValue, the
|
|
// same calls the child makes. The parent no longer exposes add/remove
|
|
// handlers.
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(0);
|
|
|
|
wrapper.vm.newSchemaFieldsForm.pushFieldValue("newSchemaFields", {
|
|
name: "",
|
|
type: "",
|
|
index_type: [],
|
|
});
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(1);
|
|
expect(wrapper.vm.newSchemaFields[0].name).toBe("");
|
|
expect(wrapper.vm.newSchemaFields[0].type).toBe("");
|
|
|
|
wrapper.vm.newSchemaFieldsForm.pushFieldValue("newSchemaFields", {
|
|
name: "",
|
|
type: "",
|
|
index_type: [],
|
|
});
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(2);
|
|
|
|
wrapper.vm.newSchemaFieldsForm.removeFieldValue("newSchemaFields", 0);
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(1);
|
|
});
|
|
|
|
it("should close dialog when last schema field is removed", async () => {
|
|
wrapper.vm.isDialogOpen = true;
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "test", type: "string", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
// The child removes rows directly via the form; the parent's watch closes
|
|
// the dialog once the rows drain to empty (main's behavior, restored).
|
|
wrapper.vm.newSchemaFieldsForm.removeFieldValue("newSchemaFields", 0);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(wrapper.vm.isDialogOpen).toBe(false);
|
|
expect(wrapper.vm.newSchemaFields).toEqual([]);
|
|
});
|
|
|
|
it("should remove a field from the missing performance fields list", () => {
|
|
wrapper.vm.missingPerformanceFields = [
|
|
{ name: "field1", type: "Full Text Search" },
|
|
{ name: "field2", type: "Secondary Index" },
|
|
];
|
|
wrapper.vm.pendingSelectedFields = ["field3"];
|
|
|
|
wrapper.vm.removeFieldFromList("fts", "field1");
|
|
|
|
expect(wrapper.vm.missingPerformanceFields).toEqual([
|
|
{ name: "field2", type: "Secondary Index" },
|
|
]);
|
|
// Should not close dialog since there's still one missing field
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
});
|
|
|
|
it("should close dialog when last missing performance field is removed", () => {
|
|
wrapper.vm.confirmAddPerformanceFieldsDialog = true;
|
|
wrapper.vm.missingPerformanceFields = [{ name: "field1", type: "Full Text Search" }];
|
|
wrapper.vm.pendingSelectedFields = ["field3"];
|
|
|
|
wrapper.vm.removeFieldFromList("fts", "field1");
|
|
|
|
expect(wrapper.vm.confirmAddPerformanceFieldsDialog).toBe(false);
|
|
expect(wrapper.vm.missingPerformanceFields).toEqual([]);
|
|
expect(wrapper.vm.pendingSelectedFields).toEqual([]);
|
|
});
|
|
|
|
it("should check if option is present in default env config", () => {
|
|
wrapper.vm.store.state.zoConfig.default_fts_keys = ["message"];
|
|
wrapper.vm.store.state.zoConfig.default_secondary_index_fields = ["status"];
|
|
|
|
expect(
|
|
wrapper.vm.checkIfOptionPresentInDefaultEnv("message", {
|
|
value: "fullTextSearchKey",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
wrapper.vm.checkIfOptionPresentInDefaultEnv("status", {
|
|
value: "secondaryIndexKey",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
wrapper.vm.checkIfOptionPresentInDefaultEnv("message", {
|
|
value: "secondaryIndexKey",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("should compute indexTypeOptionsForRow with disabled flags", () => {
|
|
const options = wrapper.vm.indexTypeOptionsForRow({
|
|
name: "message",
|
|
index_type: ["fullTextSearchKey"],
|
|
});
|
|
|
|
expect(Array.isArray(options)).toBe(true);
|
|
expect(options.length).toBeGreaterThan(0);
|
|
options.forEach((opt: any) => {
|
|
expect(opt).toHaveProperty("value");
|
|
expect(opt).toHaveProperty("label");
|
|
expect(opt).toHaveProperty("disabled");
|
|
});
|
|
});
|
|
|
|
it("should filter filterFieldFn by name", () => {
|
|
const rows = wrapper.vm.indexData.schema;
|
|
const result = wrapper.vm.filterFieldFn(rows, "message@allFields");
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("message");
|
|
});
|
|
|
|
it("should filter filterFieldFn with schema fields filter type", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["message"];
|
|
const rows = wrapper.vm.indexData.schema;
|
|
const result = wrapper.vm.filterFieldFn(rows, "message@schemaFields");
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("message");
|
|
});
|
|
|
|
it("should filter filterFieldFn with empty search returns all rows", () => {
|
|
const rows = wrapper.vm.indexData.schema;
|
|
const result = wrapper.vm.filterFieldFn(rows, "@allFields");
|
|
expect(result.length).toBe(3);
|
|
});
|
|
|
|
it("should sort stream field names", () => {
|
|
const names = wrapper.vm.streamFieldNames;
|
|
expect(Array.isArray(names)).toBe(true);
|
|
expect(names.length).toBe(3);
|
|
});
|
|
|
|
it("should compute hasUDSFieldInSelection", async () => {
|
|
wrapper.vm.activeTab = "allFields";
|
|
wrapper.vm.selectedFields = [{ name: "message", isUserDefined: true }];
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.hasUDSFieldInSelection).toBe(true);
|
|
|
|
wrapper.vm.selectedFields = [{ name: "message", isUserDefined: false }];
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.hasUDSFieldInSelection).toBe(false);
|
|
});
|
|
|
|
it("should get missing performance fields (concurrent with secondaryIndex and FTS)", () => {
|
|
// "message" has fullTextSearchKey, so it should appear in missing if not selected
|
|
const selectedFieldsSet = new Set(["_timestamp"]);
|
|
const missing = wrapper.vm.getMissingPerformanceFields(selectedFieldsSet);
|
|
|
|
const ftsMissing = missing.filter((f: any) => f.type === "Full Text Search");
|
|
expect(ftsMissing.length).toBeGreaterThan(0);
|
|
expect(ftsMissing.some((f: any) => f.name === "message")).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — onSubmit and deleteFields async paths
|
|
// ==========================================
|
|
describe("onSubmit and deleteFields", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "save-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.1,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "save-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 1.0,
|
|
compressed_size: 0.1,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should save stream settings via onSubmit", async () => {
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should handle updateSettings error in onSubmit", async () => {
|
|
mockUpdateSettings.mockRejectedValue({
|
|
response: { data: { message: "Update failed" } },
|
|
});
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockToast).toHaveBeenCalled();
|
|
expect(wrapper.vm.loadingState).toBe(false);
|
|
});
|
|
|
|
it("should show error when dataRetentionDays < 1 during onSubmit", async () => {
|
|
wrapper.vm.dataRetentionDays = 0;
|
|
mockToast.mockClear();
|
|
|
|
await wrapper.vm.onSubmit();
|
|
|
|
expect(mockToast).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: expect.stringContaining("Retention period must be at least 1 day"),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should delete fields via deleteFields", async () => {
|
|
// Simulate selected fields to delete
|
|
wrapper.vm.selectedFields = [{ name: "message" }];
|
|
wrapper.vm.confirmQueryModeChangeDialog = true;
|
|
|
|
await wrapper.vm.deleteFields();
|
|
await flushPromises();
|
|
|
|
// After successful delete, confirmDialog should be closed
|
|
expect(wrapper.vm.confirmQueryModeChangeDialog).toBe(false);
|
|
});
|
|
|
|
const findConfirmDialog = (title: string) =>
|
|
wrapper
|
|
.findAllComponents({ name: "ConfirmDialog" })
|
|
.find((d: any) => d.props("title") === title)!;
|
|
|
|
it("shows the selected field count in a red Delete Fields dialog", async () => {
|
|
wrapper.vm.selectedFields = [{ name: "message" }, { name: "level" }];
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.find('[data-test="schema-delete-button"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(wrapper.vm.confirmQueryModeChangeDialog).toBe(true);
|
|
const dialog = findConfirmDialog("Delete Fields");
|
|
expect(dialog.props("message")).toBe(
|
|
"Are you sure you want to delete 2 fields? This action cannot be undone.",
|
|
);
|
|
expect(dialog.props("okVariant")).toBe("destructive");
|
|
});
|
|
|
|
it("uses the singular form and keeps the count after the selection clears", async () => {
|
|
wrapper.vm.selectedFields = [{ name: "message" }];
|
|
wrapper.vm.openDeleteConfirm();
|
|
wrapper.vm.selectedFields = [];
|
|
await flushPromises();
|
|
|
|
expect(findConfirmDialog("Delete Field").props("message")).toBe(
|
|
"Are you sure you want to delete 1 field? This action cannot be undone.",
|
|
);
|
|
});
|
|
|
|
it("counts the selected fields in the footer in the singular and the plural", async () => {
|
|
wrapper.vm.activeMainTab = "schemaSettings";
|
|
wrapper.vm.selectedFields = [{ name: "message" }];
|
|
await flushPromises();
|
|
const footer = () => wrapper.find('[data-test="schema-fields-selected-count"]');
|
|
expect(footer().text()).toBe("1 field selected");
|
|
expect(footer().find("strong").text()).toBe("1");
|
|
|
|
wrapper.vm.selectedFields = [{ name: "message" }, { name: "level" }];
|
|
await flushPromises();
|
|
expect(footer().text()).toBe("2 fields selected");
|
|
});
|
|
|
|
it("shows the selected date range count in a red Delete Dates dialog", async () => {
|
|
wrapper.vm.activeMainTab = "redButton";
|
|
wrapper.vm.selectedDateFields = [
|
|
{ index: 0, original_start: 1, original_end: 2 },
|
|
{ index: 1, original_start: 3, original_end: 4 },
|
|
{ index: 2, original_start: 5, original_end: 6 },
|
|
];
|
|
wrapper.vm.openDeleteConfirm();
|
|
wrapper.vm.selectedDateFields = [];
|
|
await flushPromises();
|
|
|
|
expect(wrapper.vm.confirmDeleteDatesDialog).toBe(true);
|
|
const dialog = findConfirmDialog("Delete Dates");
|
|
expect(dialog.props("message")).toBe(
|
|
"Are you sure you want to delete 3 date ranges? This action cannot be undone.",
|
|
);
|
|
expect(dialog.props("okVariant")).toBe("destructive");
|
|
});
|
|
|
|
it("should handle deleteFields error gracefully", async () => {
|
|
vi.hoisted(() => ({
|
|
mockStreamServiceDeleteFields: vi.fn(),
|
|
}));
|
|
// We need to intercept the streamService mock
|
|
const streamService = (await import("@/services/stream")).default;
|
|
|
|
wrapper.vm.selectedFields = [{ name: "message" }];
|
|
|
|
// Mock the deleteFields call to reject
|
|
vi.mocked(streamService.deleteFields).mockRejectedValueOnce(new Error("Delete failed"));
|
|
|
|
await wrapper.vm.deleteFields();
|
|
await flushPromises();
|
|
|
|
expect(mockToast).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should deal with dataRetentionDays empty string error in onSubmit", async () => {
|
|
wrapper.vm.dataRetentionDays = "";
|
|
mockToast.mockClear();
|
|
|
|
await wrapper.vm.onSubmit();
|
|
|
|
expect(mockToast).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: expect.stringContaining("Retention period must be at least 1 day"),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should set flatten_level in settings when flattenLevel not null", async () => {
|
|
wrapper.vm.flattenLevel = 5;
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
"save-stream",
|
|
"logs",
|
|
expect.objectContaining({
|
|
flatten_level: 5,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Cross-linking and LLM eval tabs
|
|
// ==========================================
|
|
describe("Cross-Linking Tab", () => {
|
|
let w: any;
|
|
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "link-stream",
|
|
stream_type: "traces",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 0.5,
|
|
compressed_size: 0.05,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "span_id", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: true,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "link-stream",
|
|
stream_type: "traces",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 1678448628630259,
|
|
doc_time_max: 1678448628652947,
|
|
doc_num: 100,
|
|
file_num: 1,
|
|
storage_size: 0.5,
|
|
compressed_size: 0.05,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "span_id", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: true,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
afterEach(() => {
|
|
w.unmount();
|
|
});
|
|
|
|
it("should show cross-linking tab when enable_cross_linking is true", async () => {
|
|
store.state.zoConfig.enable_cross_linking = true;
|
|
w.vm.updateActiveMainTab("crossLinking");
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.activeMainTab).toBe("crossLinking");
|
|
expect(Array.isArray(w.vm.streamCrossLinks)).toBe(true);
|
|
expect(Array.isArray(w.vm.orgCrossLinks)).toBe(true);
|
|
});
|
|
|
|
it("should hide storeOriginalData toggle for traces", () => {
|
|
expect(w.vm.showStoreOriginalDataToggle).toBe(false);
|
|
});
|
|
|
|
it("should compute hasUDSFieldInSelection for allFields tab", async () => {
|
|
w.vm.activeTab = "allFields";
|
|
w.vm.selectedFields = [{ name: "span_id", isUserDefined: true }];
|
|
await w.vm.$nextTick();
|
|
|
|
expect(w.vm.hasUDSFieldInSelection).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — setSchema edge cases
|
|
// ==========================================
|
|
describe("setSchema edge cases", () => {
|
|
it("should populate indexData.defaultFts when FTS/popular columns are present but settings FTS is empty", async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "fts-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
const w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "fts-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
expect(w.vm.indexData.defaultFts).toBe(true);
|
|
w.unmount();
|
|
});
|
|
|
|
it("should update schema with defined_schema_fields not already in schema", async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "extra-uds-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: ["extra_field"],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
const w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "extra-uds-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: ["extra_field"],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
// "extra_field" should be added to schema
|
|
const fieldNames = w.vm.indexData.schema.map((f: any) => f.name);
|
|
expect(fieldNames).toContain("extra_field");
|
|
w.unmount();
|
|
});
|
|
|
|
it("should handle streamResponse.schema being empty with defined_schema_fields", async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "empty-schema-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: ["field1"],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
const w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "empty-schema-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: ["field1"],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
// field1 from defined_schema_fields should be in schema
|
|
const fieldNames = w.vm.indexData.schema.map((f: any) => f.name);
|
|
expect(fieldNames).toContain("field1");
|
|
// No _timestamp in original schema (it was empty array)
|
|
w.unmount();
|
|
});
|
|
});
|
|
|
|
describe("Pattern count label", () => {
|
|
const originalIsEnterprise = (config as any).isEnterprise;
|
|
let w: any;
|
|
|
|
beforeEach(() => {
|
|
(config as any).isEnterprise = "true";
|
|
});
|
|
|
|
afterEach(() => {
|
|
w?.unmount();
|
|
(config as any).isEnterprise = originalIsEnterprise;
|
|
});
|
|
|
|
it.each([
|
|
[1, "View 1 Pattern"],
|
|
[2, "View 2 Patterns"],
|
|
])("labels %i pattern(s) as %s", async (count, label) => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "test-stream",
|
|
stream_type: "logs",
|
|
stats: {},
|
|
schema: [{ name: "message", type: "Utf8" }],
|
|
settings: { full_text_search_keys: [], index_fields: [], bloom_filter_fields: [] },
|
|
pattern_associations: Array.from({ length: count }, (_, i) => ({
|
|
field: "message",
|
|
pattern_name: `p${i}`,
|
|
pattern_id: `id${i}`,
|
|
policy: "Redact",
|
|
apply_at: "AtIngestion",
|
|
})),
|
|
});
|
|
w = mount(LogStream, {
|
|
props: { open: true, modelValue: { name: "test-stream", stream_type: "logs", schema: [] } },
|
|
global: { provide: { store }, plugins: [i18n], stubs: { ODrawer: ODrawerStub } },
|
|
});
|
|
await flushPromises();
|
|
const action = w.find('[data-test="schema-field-message-pattern-action"]');
|
|
expect(action.exists()).toBe(true);
|
|
expect(action.text().trim()).toBe(label);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — OTable selection syncing
|
|
// ==========================================
|
|
describe("OTable Selection Syncing", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "selection-sync-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
{ name: "level", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "selection-sync-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "message", type: "Utf8" },
|
|
{ name: "level", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should sync selectedSchemaIds with selectedFields (set)", () => {
|
|
wrapper.vm.selectedSchemaIds = ["message"];
|
|
const fieldNames = wrapper.vm.selectedFields.map((f: any) => f.name);
|
|
expect(fieldNames).toContain("message");
|
|
// timestamp should be filtered out (equal to zoConfig.timestamp_column)
|
|
expect(fieldNames).not.toContain("_timestamp");
|
|
});
|
|
|
|
it("should sync selectedSchemaIds with selectedFields (get)", () => {
|
|
wrapper.vm.selectedFields = [
|
|
{ name: "message", type: "Utf8" },
|
|
{ name: "level", type: "Utf8" },
|
|
];
|
|
expect(wrapper.vm.selectedSchemaIds).toEqual(["message", "level"]);
|
|
});
|
|
|
|
it("should sync selectedDateIds (get) from selectedDateFields", () => {
|
|
wrapper.vm.redBtnRows = [
|
|
{ index: 0, start: "01-01-2024", end: "02-01-2024" },
|
|
{ index: 1, start: "03-01-2024", end: "04-01-2024" },
|
|
];
|
|
wrapper.vm.selectedDateFields = [{ index: 0, start: "01-01-2024", end: "02-01-2024" }];
|
|
expect(wrapper.vm.selectedDateIds).toEqual([0]);
|
|
});
|
|
|
|
it("should sync selectedDateIds (set) from ids", () => {
|
|
wrapper.vm.redBtnRows = [
|
|
{ index: 0, start: "01-01-2024", end: "02-01-2024" },
|
|
{ index: 1, start: "03-01-2024", end: "04-01-2024" },
|
|
];
|
|
wrapper.vm.selectedDateIds = [1];
|
|
expect(wrapper.vm.selectedDateFields).toEqual([
|
|
{ index: 1, start: "03-01-2024", end: "04-01-2024" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Cross-links diff in onSubmit
|
|
// ==========================================
|
|
describe("Cross-links diff in onSubmit", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "cross-link-save",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
cross_links: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "cross-link-save",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
cross_links: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should compute cross_links diff when links are added", async () => {
|
|
// Add a new cross-link
|
|
wrapper.vm.streamCrossLinks = [
|
|
{ name: "new-link", url: "https://example.com", fields: ["_timestamp"] },
|
|
];
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
"cross-link-save",
|
|
"logs",
|
|
expect.objectContaining({
|
|
cross_links: expect.objectContaining({
|
|
add: expect.arrayContaining([expect.objectContaining({ name: "new-link" })]),
|
|
remove: [],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("tracks cross_link_saved once a save with changed cross-links succeeds", async () => {
|
|
wrapper.vm.streamCrossLinks = [
|
|
{ name: "new-link", url: "https://example.com", fields: ["_timestamp"] },
|
|
];
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(analytics.track).toHaveBeenCalledWith("cross_link_saved", { scope: "stream" });
|
|
});
|
|
|
|
it("does not track cross_link_saved when the cross-links are unchanged", async () => {
|
|
vi.mocked(analytics.track).mockClear();
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
expect(analytics.track).not.toHaveBeenCalledWith("cross_link_saved", expect.anything());
|
|
});
|
|
|
|
it("does not track cross_link_saved when the save fails", async () => {
|
|
vi.mocked(analytics.track).mockClear();
|
|
wrapper.vm.streamCrossLinks = [
|
|
{ name: "new-link", url: "https://example.com", fields: ["_timestamp"] },
|
|
];
|
|
mockUpdateSettings.mockRejectedValueOnce({ response: { data: { message: "boom" } } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
expect(analytics.track).not.toHaveBeenCalledWith("cross_link_saved", expect.anything());
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — updateIndexType (exercises filterValueBasedOnEnv internally)
|
|
// ==========================================
|
|
describe("updateIndexType (exercises filterValueBasedOnEnv)", () => {
|
|
beforeEach(async () => {
|
|
store.state.zoConfig.default_fts_keys = ["message"];
|
|
store.state.zoConfig.default_secondary_index_fields = ["status"];
|
|
mockGetStream.mockResolvedValue({
|
|
name: "update-idx-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "status", type: "Utf8", index_type: ["secondaryIndexKey"] },
|
|
{ name: "level", type: "Utf8", index_type: [] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: ["status"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "update-idx-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "status", type: "Utf8", index_type: ["secondaryIndexKey"] },
|
|
{ name: "level", type: "Utf8", index_type: [] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: ["status"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should update index_type on a row and mark form dirty", () => {
|
|
const row = wrapper.vm.indexData.schema.find((f: any) => f.name === "level");
|
|
expect(row).toBeDefined();
|
|
|
|
wrapper.vm.updateIndexType({ row }, ["fullTextSearchKey", "bloomFilterKey"]);
|
|
|
|
// row.index_type should be updated (fullTextSearchKey + bloomFilterKey)
|
|
expect(row.index_type).toContain("fullTextSearchKey");
|
|
expect(row.index_type).toContain("bloomFilterKey");
|
|
expect(wrapper.vm.formDirtyFlag).toBe(true);
|
|
});
|
|
|
|
it("should filter out env-default FTS key when updating message field", () => {
|
|
const row = wrapper.vm.indexData.schema.find((f: any) => f.name === "message");
|
|
expect(row).toBeDefined();
|
|
|
|
// "message" is in default_fts_keys — fullTextSearchKey should be filtered
|
|
wrapper.vm.updateIndexType({ row }, ["fullTextSearchKey", "secondaryIndexKey"]);
|
|
|
|
// fullTextSearchKey is env-default, should be filtered out
|
|
expect(row.index_type).not.toContain("fullTextSearchKey");
|
|
// secondaryIndexKey is not an env-default for message, should remain
|
|
expect(row.index_type).toContain("secondaryIndexKey");
|
|
});
|
|
|
|
it("should filter out env-default secondary index key when updating status field", () => {
|
|
const row = wrapper.vm.indexData.schema.find((f: any) => f.name === "status");
|
|
expect(row).toBeDefined();
|
|
|
|
// "status" is in default_secondary_index_fields — secondaryIndexKey should be filtered
|
|
wrapper.vm.updateIndexType({ row }, ["fullTextSearchKey", "secondaryIndexKey"]);
|
|
|
|
expect(row.index_type).toContain("fullTextSearchKey");
|
|
expect(row.index_type).not.toContain("secondaryIndexKey");
|
|
});
|
|
|
|
it("should handle empty index_type update (clear all)", () => {
|
|
const row = wrapper.vm.indexData.schema.find((f: any) => f.name === "level");
|
|
wrapper.vm.updateIndexType({ row }, []);
|
|
|
|
expect(row.index_type).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Default props without name (created else path)
|
|
// ==========================================
|
|
describe("Default props without name", () => {
|
|
it("should handle mounting with modelValue having empty name gracefully", async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "empty-name",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
schema: [],
|
|
stats: {},
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
const w = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "empty-name",
|
|
schema: [],
|
|
stats: {},
|
|
settings: { defined_schema_fields: [] },
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
expect(w.exists()).toBe(true);
|
|
w.unmount();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — dateChangeValue try/catch
|
|
// ==========================================
|
|
describe("dateChangeValue try/catch coverage", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "date-change-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "date-change-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should enter the try block when relativeTimePeriod is null", () => {
|
|
// This exercises the try/catch path in dateChangeValue
|
|
// When selectedDate.from is set and relativeTimePeriod is null,
|
|
// the function enters the try block to convert dates.
|
|
// convertDateToTimestamp (Luxon-based) may fail in jsdom,
|
|
// exercising either the success path or the catch path.
|
|
expect(() => {
|
|
wrapper.vm.dateChangeValue({
|
|
relativeTimePeriod: null,
|
|
selectedDate: { from: "2024-01-01", to: "2024-01-02" },
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("should handle dateChangeValue with selectedDate but no relativeTimePeriod", () => {
|
|
// Should not throw even when convertDateToTimestamp fails in jsdom
|
|
wrapper.vm.dateChangeValue({
|
|
selectedDate: { from: "2024-06-15", to: "2024-06-16" },
|
|
});
|
|
// The function should complete without errors
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — handleUpdateAppliedPattern apply_at else path
|
|
// ==========================================
|
|
describe("handleUpdateAppliedPattern apply_at coverage", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "apply-at-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [
|
|
{
|
|
field: "field1",
|
|
pattern_name: "p1",
|
|
pattern_id: "id1",
|
|
policy: "replace",
|
|
apply_at: "ingestion",
|
|
},
|
|
],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "apply-at-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [
|
|
{
|
|
field: "field1",
|
|
pattern_name: "p1",
|
|
pattern_id: "id1",
|
|
policy: "replace",
|
|
apply_at: "ingestion",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should use patternIdToApplyAtMap when apply_at is undefined", () => {
|
|
// In setSchema, patternIdToApplyAtMap is populated from previousSchemaVersion
|
|
// When apply_at is undefined, the else branch at line 2587 fetches from the map
|
|
const updatedPattern = {
|
|
field: "field1",
|
|
pattern_name: "p1",
|
|
pattern_id: "id1",
|
|
apply_at: undefined, // triggers the apply_at else path
|
|
};
|
|
|
|
wrapper.vm.handleUpdateAppliedPattern(updatedPattern, "field1", "id1", "apply_at");
|
|
|
|
// After the update, apply_at should come from patternIdToApplyAtMap
|
|
expect(wrapper.vm.patternAssociations.field1[0].apply_at).toBe("ingestion");
|
|
});
|
|
|
|
it("should update apply_at when defined in the updated pattern", () => {
|
|
const updatedPattern = {
|
|
field: "field1",
|
|
pattern_name: "p1",
|
|
pattern_id: "id1",
|
|
apply_at: "query_time",
|
|
};
|
|
|
|
wrapper.vm.handleUpdateAppliedPattern(updatedPattern, "field1", "id1", "apply_at");
|
|
|
|
expect(wrapper.vm.patternAssociations.field1[0].apply_at).toBe("query_time");
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — updateDefinedSchemaFields timestamp/allFields delete paths
|
|
// ==========================================
|
|
describe("updateDefinedSchemaFields delete paths", () => {
|
|
beforeEach(async () => {
|
|
store.state.zoConfig.timestamp_column = "_timestamp";
|
|
store.state.zoConfig.all_fields_name = "_all";
|
|
mockGetStream.mockResolvedValue({
|
|
name: "delete-paths-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "_all", type: "Utf8" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "delete-paths-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "_all", type: "Utf8" },
|
|
{ name: "message", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should delete timestamp_column and allFieldsName from selectedFieldsSet", () => {
|
|
// Include _timestamp and _all in selected fields to exercise the
|
|
// delete paths at lines 2357-2361 in updateDefinedSchemaFields
|
|
wrapper.vm.selectedFields = [{ name: "_timestamp" }, { name: "_all" }, { name: "message" }];
|
|
wrapper.vm.activeTab = "allFields";
|
|
|
|
wrapper.vm.updateDefinedSchemaFields();
|
|
|
|
// _timestamp and _all should be filtered from defined_schema_fields
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("_timestamp");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).not.toContain("_all");
|
|
expect(wrapper.vm.indexData.defined_schema_fields).toContain("message");
|
|
});
|
|
|
|
it("should call updateResultTotal with activeTab as schemaFields", () => {
|
|
// updateResultTotal is internal (not exported from setup).
|
|
// Test the behavior indirectly by calling updateStreamResponse
|
|
// with activeTab set to "schemaFields" — this exercises
|
|
// the resultTotal = defined_schema_fields.length branch
|
|
wrapper.vm.activeTab = "schemaFields";
|
|
wrapper.vm.indexData.defined_schema_fields = ["field1", "field2"];
|
|
|
|
const mockResponse = {
|
|
settings: { defined_schema_fields: ["field1", "field2"] },
|
|
schema: [{ name: "field1" }, { name: "field2" }, { name: "field3" }],
|
|
};
|
|
|
|
wrapper.vm.updateStreamResponse(mockResponse);
|
|
|
|
// activeTab is "schemaFields", so resultTotal should be defined_schema_fields length
|
|
expect(wrapper.vm.resultTotal).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — deleteFields error branch (res.data.code != 200)
|
|
// ==========================================
|
|
describe("deleteFields non-200 response", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "del-non200-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "field_to_delete", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "del-non200-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64" },
|
|
{ name: "field_to_delete", type: "Utf8" },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should show error toast when deleteFields returns non-200 code", async () => {
|
|
const { mockStreamServiceDeleteFields } = await (async () => {
|
|
const mod = await import("@/services/stream");
|
|
return { mockStreamServiceDeleteFields: mod.default.deleteFields };
|
|
})();
|
|
|
|
mockToast.mockClear();
|
|
vi.mocked(mockStreamServiceDeleteFields).mockResolvedValueOnce({
|
|
data: { code: 400, message: "Cannot delete system field" },
|
|
});
|
|
|
|
wrapper.vm.selectedFields = [{ name: "field_to_delete" }];
|
|
|
|
await wrapper.vm.deleteFields();
|
|
await flushPromises();
|
|
|
|
// Should toast the error message from the response
|
|
expect(mockToast).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: "Cannot delete system field",
|
|
}),
|
|
);
|
|
expect(wrapper.vm.confirmQueryModeChangeDialog).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — event handler: enforceMaxIndexTypes in template
|
|
// ==========================================
|
|
describe("enforceMaxIndexTypes template integration", () => {
|
|
it("should cap index type at 2 when exceeding max via enforceMaxIndexTypes + updateIndexType", () => {
|
|
// Simulate the template: enforceMaxIndexTypes wraps the value before
|
|
// passing to updateIndexType, mirroring:
|
|
// @update:model-value="(val) => updateIndexType({ row }, enforceMaxIndexTypes(val))"
|
|
const capped = wrapper.vm.enforceMaxIndexTypes([
|
|
"fullTextSearchKey",
|
|
"secondaryIndexKey",
|
|
"bloomFilterKey",
|
|
]);
|
|
expect(capped.length).toBe(2);
|
|
|
|
const row = { name: "field1", index_type: [] };
|
|
wrapper.vm.updateIndexType({ row }, capped);
|
|
expect(row.index_type.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — handleUpdateAppliedPattern with null apply_at
|
|
// ==========================================
|
|
describe("handleUpdateAppliedPattern null apply_at", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "null-apply-at-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "null-apply-at-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should handle updateAppliedPattern with null apply_at (else branch line 2587)", () => {
|
|
wrapper.vm.patternAssociations = {
|
|
fieldX: [
|
|
{
|
|
field: "fieldX",
|
|
pattern_name: "px",
|
|
pattern_id: "pidX",
|
|
policy: "replace",
|
|
apply_at: "ingestion",
|
|
},
|
|
],
|
|
};
|
|
const pattern = {
|
|
field: "fieldX",
|
|
pattern_name: "px",
|
|
pattern_id: "pidX",
|
|
policy: "replace",
|
|
apply_at: null,
|
|
};
|
|
|
|
wrapper.vm.handleUpdateAppliedPattern(pattern, "fieldX", "pidX", "apply_at");
|
|
|
|
const updatedPattern = wrapper.vm.patternAssociations.fieldX[0];
|
|
expect(updatedPattern.apply_at).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — filterFieldFn index_type matching
|
|
// ==========================================
|
|
describe("filterFieldFn index_type matching", () => {
|
|
beforeEach(async () => {
|
|
store.state.zoConfig.timestamp_column = "_timestamp";
|
|
mockGetStream.mockResolvedValue({
|
|
name: "filter-idx-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "status", type: "Utf8", index_type: ["secondaryIndexKey"] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: ["status"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "filter-idx-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [
|
|
{ name: "_timestamp", type: "Int64", index_type: [] },
|
|
{ name: "message", type: "Utf8", index_type: ["fullTextSearchKey"] },
|
|
{ name: "status", type: "Utf8", index_type: ["secondaryIndexKey"] },
|
|
],
|
|
settings: {
|
|
full_text_search_keys: ["message"],
|
|
index_fields: ["status"],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should match by index_type value in filterFieldFn", () => {
|
|
// Filter by a direct index_type value match (exercises lines 2060-2066)
|
|
const rows = wrapper.vm.indexData.schema;
|
|
const result = wrapper.vm.filterFieldFn(rows, "fullTextSearchKey@allFields");
|
|
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("message");
|
|
});
|
|
|
|
it("should match by index_type label in filterFieldFn", () => {
|
|
// Filter by an index type label that maps to a value
|
|
const rows = wrapper.vm.indexData.schema;
|
|
const result = wrapper.vm.filterFieldFn(rows, "full text search@allFields");
|
|
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("message");
|
|
});
|
|
|
|
it("should match by index_type value in schemaFields tab", () => {
|
|
// Exercise the schemaFields index_type matching path (lines 2036-2045)
|
|
wrapper.vm.indexData.defined_schema_fields = ["message", "status"];
|
|
const rows = wrapper.vm.indexData.schema;
|
|
|
|
// Search for "secondaryIndexKey" in schemaFields tab
|
|
// "status" has secondaryIndexKey and is in defined_schema_fields
|
|
const result = wrapper.vm.filterFieldFn(rows, "secondaryIndexKey@schemaFields");
|
|
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("status");
|
|
});
|
|
|
|
it("should match by index_type label (label->value map) in schemaFields tab", () => {
|
|
wrapper.vm.indexData.defined_schema_fields = ["message"];
|
|
const rows = wrapper.vm.indexData.schema;
|
|
|
|
const result = wrapper.vm.filterFieldFn(rows, "full text search@schemaFields");
|
|
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("message");
|
|
});
|
|
|
|
it("should return all schemaFields when search term is empty", () => {
|
|
// Exercises line 2029: filterFieldFn with empty search in schemaFields tab
|
|
wrapper.vm.indexData.defined_schema_fields = ["message", "status"];
|
|
const rows = wrapper.vm.indexData.schema;
|
|
|
|
const result = wrapper.vm.filterFieldFn(rows, "@schemaFields");
|
|
|
|
expect(result.length).toBe(2);
|
|
expect(result.map((r: any) => r.name).sort()).toEqual(["message", "status"]);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — disableOptions hashPartition conflict with keyPartition/prefixPartition
|
|
// ==========================================
|
|
describe("disableOptions hashPartition-keyPartition/prefixPartition conflict", () => {
|
|
it("should disable hashPartition when row already has keyPartition", () => {
|
|
const schema = { name: "field1", index_type: ["keyPartition"] };
|
|
const hashOption = { value: "hashPartition_8" };
|
|
|
|
expect(wrapper.vm.disableOptions(schema, hashOption)).toBe(true);
|
|
});
|
|
|
|
it("should disable hashPartition when row already has prefixPartition", () => {
|
|
const schema = { name: "field2", index_type: ["prefixPartition"] };
|
|
const hashOption = { value: "hashPartition_16" };
|
|
|
|
expect(wrapper.vm.disableOptions(schema, hashOption)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Phase 2 tests — Exercise remaining computed/functions for coverage
|
|
// ==========================================
|
|
describe("Coverage-closing test: exercise setup-level functions", () => {
|
|
beforeEach(async () => {
|
|
mockGetStream.mockResolvedValue({
|
|
name: "coverage-close-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
partition_keys: {},
|
|
},
|
|
pattern_associations: [],
|
|
});
|
|
wrapper = mount(LogStream, {
|
|
props: {
|
|
modelValue: {
|
|
name: "coverage-close-stream",
|
|
stream_type: "logs",
|
|
storage_type: "s3",
|
|
stats: {
|
|
doc_time_min: 0,
|
|
doc_time_max: 0,
|
|
doc_num: 0,
|
|
file_num: 0,
|
|
storage_size: 0,
|
|
compressed_size: 0,
|
|
},
|
|
schema: [{ name: "_timestamp", type: "Int64" }],
|
|
settings: {
|
|
full_text_search_keys: [],
|
|
index_fields: [],
|
|
bloom_filter_fields: [],
|
|
data_retention: 30,
|
|
max_query_range: 0,
|
|
store_original_data: false,
|
|
approx_partition: false,
|
|
defined_schema_fields: [],
|
|
extended_retention_days: [],
|
|
partition_keys: {},
|
|
pattern_associations: [],
|
|
},
|
|
},
|
|
},
|
|
global: {
|
|
provide: { store },
|
|
plugins: [i18n],
|
|
stubs: { ODrawer: ODrawerStub },
|
|
},
|
|
});
|
|
await flushPromises();
|
|
});
|
|
|
|
it("should exercise onSubmit with non-empty newSchemaFields (maps)", async () => {
|
|
// Func 55, 56: newSchemaFields map callbacks in onSubmit. The row gate
|
|
// validates the NORMALIZED name, so a dash is allowed — "test-field"
|
|
// normalizes to "test_field" (valid), passes the gate, and is saved.
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "test-field", type: "Utf8", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
// newSchemaFields should be reset after successful save
|
|
expect(wrapper.vm.newSchemaFields).toEqual([]);
|
|
});
|
|
|
|
it("should block onSubmit when a field name has invalid characters", async () => {
|
|
// Option A gating: "!" survives normalization, so "user!id" fails the row
|
|
// schema. onSubmit reveals the inline error and does NOT save — the fix for
|
|
// the dropped inline feedback (invalid names no longer pass silently).
|
|
wrapper.vm.isDialogOpen = true;
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "user!id", type: "Utf8", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
mockUpdateSettings.mockClear();
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
|
// Rows are preserved (not reset) so the user can correct them.
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(1);
|
|
});
|
|
|
|
it("should block onSubmit when a field has no data type", async () => {
|
|
// The data-type select is visible + required in the Add Fields card, so a
|
|
// row with a valid name but no type also blocks the save.
|
|
wrapper.vm.isDialogOpen = true;
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "valid_name", type: "", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
mockUpdateSettings.mockClear();
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
|
expect(wrapper.vm.newSchemaFields.length).toBe(1);
|
|
});
|
|
|
|
it("should save on Enter from a field-name input", async () => {
|
|
// Enter in the Add Fields name input triggers the settings save (the
|
|
// nested <form> otherwise swallows Enter with 2+ rows).
|
|
wrapper.vm.isDialogOpen = true;
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "valid_name", type: "Utf8", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
mockUpdateSettings.mockClear();
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onAddFieldsKeyup({
|
|
key: "Enter",
|
|
target: { tagName: "INPUT", name: "newSchemaFields[0].name" },
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should NOT save on Enter from the data-type select (only the name input submits)", async () => {
|
|
wrapper.vm.isDialogOpen = true;
|
|
wrapper.vm.newSchemaFieldsForm.setFieldValue("newSchemaFields", [
|
|
{ name: "valid_name", type: "Utf8", index_type: [] },
|
|
]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
mockUpdateSettings.mockClear();
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
// Enter on the type field (dropdown) must not submit.
|
|
wrapper.vm.onAddFieldsKeyup({
|
|
key: "Enter",
|
|
target: { tagName: "INPUT", name: "newSchemaFields[0].type" },
|
|
});
|
|
// A non-Enter key on the name input must not submit either.
|
|
wrapper.vm.onAddFieldsKeyup({
|
|
key: "a",
|
|
target: { tagName: "INPUT", name: "newSchemaFields[0].name" },
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should exercise cross-links diff in onSubmit with modified links", async () => {
|
|
// Set up cross-links to exercise the diff computation
|
|
// (func 61: prevCrossLinks.map, func 64: currCrossLinks.filter, func 65: inline fn)
|
|
wrapper.vm.streamCrossLinks = [{ name: "a", url: "https://a.com", fields: ["_timestamp"] }];
|
|
// prevCrossLinks has "a" in it (from settings.cross_links which was set in previousSchemaVersion)
|
|
// streamCrossLinks has "a" — so prevCrossLinkNames includes "a" and currCrossLinkNames includes "a"
|
|
// This exercises the diff path
|
|
mockUpdateSettings.mockResolvedValue({ data: { code: 200 } });
|
|
|
|
await wrapper.vm.onSubmit();
|
|
await flushPromises();
|
|
|
|
// Should have been called
|
|
expect(mockUpdateSettings).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|