Files
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
Every first-party Python caller and test now names the mount the API actually
answers on. Mechanical: URL literals only, and only where the /api/ was ours --
a preceding word character means it was a module path like insights/api/ or a
third-party host like slack.com/api/, and those are untouched.

Left saying /api/, each for a reason rather than for safety:
- products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints
- services/llm-gateway deploys separately and is under a code freeze, so moving
  its two calls would break it during the skew
- the mcp_store and Slack/Teams redirect_uris are registered with third parties
  and must byte-match what was registered
- the Wizard and Raycast client-metadata paths are OAuth client_ids
- /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the
  ingress routes to the Rust fleets, held by released SDK versions

Test URLconfs moved with the requests that call them, and the Datastore
user-selection fixtures now carry the leading slash that request.path has.
2026-08-08 18:38:44 -07:00

228 lines
9.0 KiB
TypeScript

import { router } from 'kea-router'
import { expectLogic } from 'kea-test-utils'
import { useMocks } from '~/mocks/jest'
import { initKeaTests } from '~/test/init'
import { logsSceneLogic } from './logsSceneLogic'
describe('logsSceneLogic', () => {
let logic: ReturnType<typeof logsSceneLogic.build>
beforeEach(async () => {
useMocks({
post: {
'/v1/environments/:team_id/logs/query/': () => [200, { results: [], maxExportableLogs: 5000 }],
'/v1/environments/:team_id/logs/sparkline/': () => [200, []],
},
})
initKeaTests()
logic = logsSceneLogic()
logic.mount()
await expectLogic(logic).toFinishAllListeners()
})
afterEach(() => {
logic.unmount()
})
describe('URL parameter parsing', () => {
it.each([
['JSON string array', '["error","warn"]', ['error', 'warn']],
['single item JSON array', '["info"]', ['info']],
['empty JSON array', '[]', []],
])('parses severityLevels from %s', async (_, urlValue, expected) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { severityLevels: urlValue })
}).toFinishAllListeners()
expect(logic.values.filters.severityLevels).toEqual(expected)
})
it.each([
['JSON string array', '["my-service","other-service"]', ['my-service', 'other-service']],
['single item JSON array', '["api"]', ['api']],
])('parses serviceNames from %s', async (_, urlValue, expected) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { serviceNames: urlValue })
}).toFinishAllListeners()
expect(logic.values.filters.serviceNames).toEqual(expected)
})
it('filters out malformed JSON as invalid severity level', async () => {
await expectLogic(logic, () => {
router.actions.push('/logs', { severityLevels: 'not-valid-json[' })
}).toFinishAllListeners()
// parseTagsFilter falls back to comma-separated parsing, then validation filters invalid levels
expect(logic.values.filters.severityLevels).toEqual([])
})
it('filters out non-array JSON as invalid severity level', async () => {
await expectLogic(logic, () => {
router.actions.push('/logs', { severityLevels: '"just-a-string"' })
}).toFinishAllListeners()
// parseTagsFilter falls back to comma-separated parsing, then validation filters invalid levels
expect(logic.values.filters.severityLevels).toEqual([])
})
it('handles comma-separated values via parseTagsFilter', async () => {
await expectLogic(logic, () => {
router.actions.push('/logs', { severityLevels: 'error,warn,info' })
}).toFinishAllListeners()
expect(logic.values.filters.severityLevels).toEqual(['error', 'warn', 'info'])
})
it.each([
['a valid lens', 'patterns', 'patterns'],
['an unrecognised lens falls back to the default', 'nonsense', 'logs'],
])('applies viewMode from the URL: %s', async (_, urlValue, expected) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { viewMode: urlValue })
}).toFinishAllListeners()
expect(logic.values.viewMode).toEqual(expected)
})
it('syncs lens switches back to the URL, dropping the param for the default lens', async () => {
// The round-trip contract for shareable lens links: switching to Patterns writes
// ?viewMode=patterns, and returning to Logs (the default) removes the param
// instead of pinning viewMode=logs into every copied URL.
await expectLogic(logic, () => {
logic.actions.setViewMode('patterns')
}).toFinishAllListeners()
expect(router.values.searchParams.viewMode).toEqual('patterns')
await expectLogic(logic, () => {
logic.actions.setViewMode('logs')
}).toFinishAllListeners()
expect(router.values.searchParams.viewMode).toBeUndefined()
})
it.each([
['completely invalid value', '["invalid-level"]', []],
['typo in valid level', '["debug123"]', []],
['mix of valid and invalid', '["error","not-a-level","warn"]', ['error', 'warn']],
['invalid comma-separated', 'invalid,also-invalid', []],
])('filters out invalid severity levels (%s)', async (_, urlValue, expected) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { severityLevels: urlValue })
}).toFinishAllListeners()
expect(logic.values.filters.severityLevels).toEqual(expected)
})
it('parses a stringified filterGroup from the URL (e.g. a cross-product session link)', async () => {
const filterGroup = {
type: 'AND',
values: [{ type: 'OR', values: [{ key: 'insightsSessionId', value: ['sess-1'], operator: 'exact' }] }],
}
await expectLogic(logic, () => {
router.actions.push('/logs', { filterGroup: JSON.stringify(filterGroup) })
}).toFinishAllListeners()
expect(logic.values.filters.filterGroup).toEqual(filterGroup)
})
it('ignores a malformed filterGroup in the URL', async () => {
const before = logic.values.filters.filterGroup
await expectLogic(logic, () => {
router.actions.push('/logs', { filterGroup: '{not valid json' })
}).toFinishAllListeners()
expect(logic.values.filters.filterGroup).toEqual(before)
})
})
describe('activeTab URL sync', () => {
it('defaults to viewer', () => {
expect(logic.values.activeTab).toEqual('viewer')
})
it.each([
['viewer', 'viewer'],
['anomalies', 'anomalies'],
['configuration', 'configuration'],
])('parses valid activeTab "%s" from URL', async (urlValue, expected) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { activeTab: urlValue })
}).toFinishAllListeners()
expect(logic.values.activeTab).toEqual(expected)
})
it.each([
['unknown string', 'invalid'],
['array', ['viewer']],
['object', { key: 'viewer' }],
['number', 42],
])('ignores invalid activeTab (%s)', async (_, urlValue) => {
await expectLogic(logic, () => {
router.actions.push('/logs', { activeTab: urlValue })
}).toFinishAllListeners()
expect(logic.values.activeTab).toEqual('viewer')
})
it('syncs activeTab to URL on setActiveTab', async () => {
await expectLogic(logic, () => {
logic.actions.setActiveTab('configuration')
}).toFinishAllListeners()
expect(logic.values.activeTab).toEqual('configuration')
expect(router.values.searchParams).toHaveProperty('activeTab', 'configuration')
})
it('removes activeTab from URL when set to default', async () => {
// First set to non-default
await expectLogic(logic, () => {
logic.actions.setActiveTab('configuration')
}).toFinishAllListeners()
// Then set back to default
await expectLogic(logic, () => {
logic.actions.setActiveTab('viewer')
}).toFinishAllListeners()
expect(logic.values.activeTab).toEqual('viewer')
expect(router.values.searchParams).not.toHaveProperty('activeTab')
})
})
describe('facetNameSearch URL sync', () => {
it('parses facetNameSearch from URL', async () => {
await expectLogic(logic, () => {
router.actions.push('/logs', { facetNameSearch: 'namespace' })
}).toFinishAllListeners()
expect(logic.values.facetNameSearch).toEqual('namespace')
})
it('syncs facetNameSearch to URL on setFacetNameSearch', async () => {
await expectLogic(logic, () => {
logic.actions.setFacetNameSearch('kube')
}).toFinishAllListeners()
expect(logic.values.facetNameSearch).toEqual('kube')
expect(router.values.searchParams).toHaveProperty('facetNameSearch', 'kube')
})
it('removes facetNameSearch from URL when cleared', async () => {
await expectLogic(logic, () => {
logic.actions.setFacetNameSearch('kube')
}).toFinishAllListeners()
await expectLogic(logic, () => {
logic.actions.setFacetNameSearch('')
}).toFinishAllListeners()
expect(logic.values.facetNameSearch).toEqual('')
expect(router.values.searchParams).not.toHaveProperty('facetNameSearch')
})
})
})