Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,937 changes: 3,957 additions & 3,980 deletions .secrets.baseline

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2025-2026 mrveiss
// SPDX-License-Identifier: Apache-2.0
/**
* SecretsManager offers only what the server enforces (#16450), and its
* categories cover every canonical secret kind (#15008).
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import { createPinia, setActivePinia } from 'pinia'
import en from '@/i18n/locales/en.json'
import SecretsManager from '../security/SecretsManager.vue'

const createSecret = vi.fn().mockResolvedValue({})
const logSecretUsage = vi.fn()
let isAdmin = false

vi.mock('@/utils/SecretsApiClient', () => ({
secretsApiClient: {
getSecrets: vi.fn().mockResolvedValue({ secrets: [] }),
getSecretsStats: vi.fn().mockResolvedValue({ total: 0, by_type: {}, by_scope: {} }),
createSecret: (...args: unknown[]) => createSecret(...args),
getSecret: vi.fn(),
updateSecret: vi.fn(),
deleteSecret: vi.fn(),
transferSecrets: vi.fn(),
},
}))
vi.mock('@/composables/security/useSecretsInfraApi', () => ({
useSecretsInfraApi: () => ({
fetchInfraHosts: vi.fn().mockResolvedValue({ hosts: [] }),
fetchSecretsUsage: vi.fn().mockResolvedValue({}),
deleteInfraHost: vi.fn(),
}),
}))
vi.mock('@/stores/useUserStore', () => ({
useUserStore: () => ({
get isAdmin() {
return isAdmin
},
}),
}))
vi.mock('@/composables/useSessionActivityLogger', () => ({
useSessionActivityLogger: () => ({ logSecretUsage: (...args: unknown[]) => logSecretUsage(...args) }),
}))
vi.mock('@/utils/debugUtils', () => ({
createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
}))

const i18n = createI18n({ legacy: false, locale: 'en', messages: { en }, missingWarn: false, fallbackWarn: false })

type ManagerVm = {
openCreateModal: () => void
saveSecret: () => Promise<void>
secretForm: Record<string, unknown>
credentialCategories: Array<{ type: string; label: string }>
viewingSecret: Record<string, unknown> | null
toggleSecretValue: () => void
copySecretValue: () => Promise<void>
getTypeLabel: (type?: string) => string
}

async function mountManager() {
const wrapper = mount(SecretsManager, {
global: {
plugins: [i18n],
stubs: {
BaseModal: { template: '<div class="modal-stub"><slot /><slot name="footer" /></div>' },
ShareSecretDialog: true,
Teleport: true,
},
},
})
await flushPromises()
return { wrapper, vm: wrapper.vm as unknown as ManagerVm }
}

describe('SecretsManager access controls (#16450)', () => {
beforeEach(() => {
setActivePinia(createPinia())
isAdmin = false
createSecret.mockClear()
})

it('offers no Visibility / Organization / Team / Shared-With control in the create form', async () => {
const { wrapper, vm } = await mountManager()
vm.openCreateModal()
// The form proper renders once a kind is chosen (before that: template picker).
vm.secretForm.type = 'api_key'
await flushPromises()

// Contrast: the form did render -- its Scope control is there.
expect(wrapper.find('#secret-scope').exists()).toBe(true)
for (const id of ['#secret-visibility', '#secret-org-id', '#secret-team-ids', '#secret-shared-with']) {
expect(wrapper.find(id).exists()).toBe(false)
}
})

it('sends no access-control field the server would silently drop', async () => {
const { vm } = await mountManager()
vm.openCreateModal()
Object.assign(vm.secretForm, { type: 'api_key', name: 'OPENAI_API_KEY', scope: 'general', value: 'sk-test' })
await vm.saveSecret()

expect(createSecret).toHaveBeenCalledTimes(1)
const payload = createSecret.mock.calls[0][0] as Record<string, unknown>
expect(payload).toMatchObject({ type: 'api_key', name: 'OPENAI_API_KEY', scope: 'general' })
for (const field of ['visibility', 'org_id', 'team_ids', 'shared_with']) {
expect(payload).not.toHaveProperty(field)
}
})
})

describe('SecretsManager categories (#15008)', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

it('offers a translated category for every canonical kind, including the three that were missing', async () => {
isAdmin = true
const { vm } = await mountManager()
const byType = Object.fromEntries(vm.credentialCategories.map((c) => [c.type, c.label]))

expect(Object.keys(byType)).toHaveLength(10)
expect(byType.oauth_refresh_token).toBe(en.security.secretsManager.categories.oauth_refresh_token)
expect(byType.connector_oauth_token).toBe(en.security.secretsManager.categories.connector_oauth_token)
expect(byType.infrastructure_host).toBe(en.security.secretsManager.categories.infrastructure_host)
})

it('keeps the admin-only infrastructure category from a non-admin', async () => {
isAdmin = false
const { vm } = await mountManager()
expect(vm.credentialCategories.map((c) => c.type)).not.toContain('infrastructure_host')
})

it('labels a single secret by its translated kind, not by trimming a plural', async () => {
const { vm } = await mountManager()
expect(vm.getTypeLabel('connector_oauth_token')).toBe(en.security.secretsManager.kinds.connector_oauth_token)
expect(vm.getTypeLabel('some_future_kind')).toBe('Some Future Kind')
})
})

describe('SecretsManager session usage logging (#17976)', () => {
beforeEach(() => {
setActivePinia(createPinia())
logSecretUsage.mockClear()
})

it('logs a reveal once when the value is shown, not when it is hidden again', async () => {
const { vm } = await mountManager()
vm.viewingSecret = { id: 's1', name: 'CONNECTOR', type: 'connector_oauth_token', value: 'v' }

vm.toggleSecretValue()
vm.toggleSecretValue()

expect(logSecretUsage).toHaveBeenCalledTimes(1)
expect(logSecretUsage).toHaveBeenCalledWith('reveal', 's1', 'CONNECTOR', 'connector_oauth_token')
})

it('logs a copy after the value reaches the clipboard', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true })
const { vm } = await mountManager()
vm.viewingSecret = { id: 's2', name: 'KEY', type: 'api_key', value: 'v' }

await vm.copySecretValue()

expect(writeText).toHaveBeenCalledWith('v')
expect(logSecretUsage).toHaveBeenCalledWith('copy', 's2', 'KEY', 'api_key')
})

it('logs an unrecognised server kind as other rather than casting it through', async () => {
const { vm } = await mountManager()
vm.viewingSecret = { id: 's3', name: 'X', type: 'some_future_kind', value: 'v' }

vm.toggleSecretValue()

expect(logSecretUsage).toHaveBeenCalledWith('reveal', 's3', 'X', 'other')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
<!-- Duplicate Code Analysis (#1469, #184) -->
<DuplicatesSection
:duplicates="duplicateAnalysis"
:scan-state="duplicateScanState"
:loading="loadingProgress.duplicates"
@export="(fmt: string) => exportSection('duplicates', fmt as 'md' | 'json')"
/>
Expand Down Expand Up @@ -574,6 +575,7 @@ const {
codebaseStats,
problemsReport,
duplicateAnalysis,
duplicateScanState,
declarationAnalysis,
hardcodeAnalysis,
loadingProgress,
Expand Down
14 changes: 14 additions & 0 deletions autobot-frontend/src/components/analytics/DuplicatesSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@
</div>
</div>
</div>
<!-- #17983: a failed or never-run scan is not "no duplicates found" -->
<EmptyState
v-else-if="scanState === 'failed'"
icon="exclamation-triangle"
:message="$t('analytics.duplicates.scanFailed')"
/>
<EmptyState
v-else-if="scanState === 'not_scanned'"
icon="search"
:message="$t('analytics.duplicates.notScanned')"
/>
<EmptyState
v-else-if="!loading"
icon="check-circle"
Expand All @@ -117,6 +128,7 @@ import Icon from '@/components/ui/Icon.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import EmptyState from '@/components/ui/EmptyState.vue'
import type { DuplicateScanState } from '@/composables/analytics/duplicatePayload'
import { useAggregationMemo } from '@/composables/useComputedMemo'
import { useExpansion } from '@/composables/useExpansion'

Expand All @@ -133,6 +145,8 @@ interface Props {
duplicates: Duplicate[]
/** #5368: render a spinner during the scan instead of empty-state. */
loading?: boolean
/** #17983: done | not_scanned | failed -- only "done" may show "none found". */
scanState?: DuplicateScanState
}

const props = defineProps<Props>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025-2026 mrveiss
// SPDX-License-Identifier: Apache-2.0
/**
* DuplicatesSection shows "no duplicates" only for a scan that ran (#17983).
*/
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import en from '@/i18n/locales/en.json'
import DuplicatesSection from '../DuplicatesSection.vue'

const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } })
const msg = en.analytics.duplicates
const render = (scanState?: 'done' | 'not_scanned' | 'failed') =>
mount(DuplicatesSection, { props: { duplicates: [], scanState }, global: { plugins: [i18n] } }).text()

describe('DuplicatesSection scan states (#17983)', () => {
it('a failed scan says so, and never "no duplicates"', () => {
const text = render('failed')
expect(text).toContain(msg.scanFailed)
expect(text).not.toContain(msg.emptyMessage)
})

it('a never-run scan says it has not run', () => {
const text = render('not_scanned')
expect(text).toContain(msg.notScanned)
expect(text).not.toContain(msg.emptyMessage)
})

it('a completed scan with nothing found shows "no duplicates"', () => {
expect(render('done')).toContain(msg.emptyMessage)
})
})
Loading
Loading