Files
shopdb-flask/plugins/endpoints/frontend/views/agentKeySettings.spec.js
cproudlock 22d06abb70 Show per-PC credentials across the admin interface
- Endpoints (shared key) page: the key is presented as what PCs use until
  they enroll, with enrollment progress, a link to Enrollment, rewritten
  guidance, and rotate/revoke confirmations that say how many PCs are not
  enrolled yet.
- Settings cards describe the server certificate as what PCs actually
  receive, on IIS or Docker; Enrollment has its own icon (the icon set lacked
  the one the Server Certificate card already asked for).
- Server Certificate page checks the certificate the site presents at its
  public address and compares its pin with the certificate file, warning when
  they differ. The host comes from site settings only; the check is a TLS
  handshake, never an HTTP request.
- Fleet list: Credential column and filter (enrolled, pending, re-enroll
  blocked, shared key, not reporting), computed in the list query.
- PC detail: credential state, prefix and dates for fleet readers, state only
  for others, with admin Revoke (optionally blocking re-enrollment) and Allow
  re-enrollment.
- Dashboard: pending enrollment requests, and PCs blocked from re-enrolling
  for more than seven days.
- Runbook: where to find each of these in the app.
2026-09-15 16:07:06 -04:00

134 lines
5.3 KiB
JavaScript

/**
* Endpoints (shared key) settings page.
*
* The shared key only matters to PCs that have not enrolled, so the page must
* say how far enrollment has got, and the regenerate and revoke confirms must
* name how many PCs the change actually reaches.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises, RouterLinkStub } from '@vue/test-utils'
vi.mock('@/api', () => ({
default: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() }
}))
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
import api from '@/api'
import AgentKeySettings from './AgentKeySettings.vue'
const KEY = { tokenprefix: 'abcd', createddate: null, lastusedat: null }
function enrollmentPayload(settings = { sharedkeyallowed: true, autoenroll: true }) {
return {
data: {
data: {
settings,
counts: { enrolled: 30, notenrolled: 8, pending: 2 },
computers: [],
requests: [
{ requestid: 1, status: 'pending', reason: 'serial mismatch' },
{ requestid: 2, status: 'pending', reason: 'awaiting approval' },
{ requestid: 3, status: 'pending', reason: 'reenroll blocked' },
{ requestid: 4, status: 'issued', reason: null }
]
}
}
}
}
async function render(payload = enrollmentPayload()) {
api.get.mockImplementation((path) => {
if (path === '/endpoints/agentkey') {
return Promise.resolve({ data: { data: { key: KEY, envkeyfallback: false } } })
}
if (path === '/endpoints/enrollment') return Promise.resolve(payload)
return Promise.resolve({ data: { data: {} } })
})
api.post.mockResolvedValue({ data: { data: { key: KEY } } })
api.delete.mockResolvedValue({ data: { data: { key: null } } })
const wrapper = mount(AgentKeySettings, { global: { stubs: { RouterLink: RouterLinkStub } } })
await flushPromises()
return wrapper
}
describe('AgentKeySettings shared key section', () => {
beforeEach(() => {
api.get.mockReset()
api.post.mockReset()
api.delete.mockReset()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('titles the section for the per-PC model', async () => {
const wrapper = await render()
expect(wrapper.text()).toContain('Shared key (used until PCs enroll)')
expect(wrapper.text()).not.toContain('Key management')
expect(wrapper.text()).toMatch(/Enrolled PCs ignore it/)
expect(wrapper.text()).toMatch(/At cutover/)
})
it('shows enrollment progress from one small enrollment read', async () => {
const wrapper = await render()
expect(api.get).toHaveBeenCalledWith('/endpoints/enrollment', { params: { perpage: 1 } })
const summary = wrapper.find('.enrollment-summary').text()
expect(summary).toContain('30 of 40 PCs enrolled')
expect(summary).toContain('10 still on the shared key')
// A blocked PC asking again is not a request anyone has to decide.
expect(summary).toContain('2 pending enrollment requests')
const link = wrapper.findComponent(RouterLinkStub)
expect(link.props('to')).toEqual({ name: 'endpoints-enrollment-settings' })
})
it('regenerate confirm names the PCs that need the new key', async () => {
const confirmSpy = vi.fn(() => true)
vi.stubGlobal('confirm', confirmSpy)
const wrapper = await render()
const button = wrapper.findAll('button').find(b => b.text() === 'Regenerate key')
await button.trigger('click')
await flushPromises()
expect(confirmSpy.mock.calls[0][0]).toMatch(/10 PCs not enrolled will need the new key/)
expect(confirmSpy.mock.calls[0][0]).toMatch(/Enrolled PCs are not affected/)
expect(api.post).toHaveBeenCalledWith('/endpoints/agentkey/regenerate')
})
it('revoke confirm names the PCs that lose access', async () => {
const confirmSpy = vi.fn(() => true)
vi.stubGlobal('confirm', confirmSpy)
const wrapper = await render()
const button = wrapper.findAll('button').find(b => b.text() === 'Revoke key')
await button.trigger('click')
await flushPromises()
expect(confirmSpy.mock.calls[0][0]).toMatch(/10 PCs not enrolled will lose access/)
expect(api.delete).toHaveBeenCalledWith('/endpoints/agentkey')
})
it('sends nothing when revoke is declined', async () => {
vi.stubGlobal('confirm', vi.fn(() => false))
const wrapper = await render()
const button = wrapper.findAll('button').find(b => b.text() === 'Revoke key')
await button.trigger('click')
await flushPromises()
expect(api.delete).not.toHaveBeenCalled()
})
it('still manages the key when the enrollment read fails', async () => {
api.get.mockImplementation((path) => {
if (path === '/endpoints/enrollment') return Promise.reject(new Error('403'))
if (path === '/endpoints/agentkey') {
return Promise.resolve({ data: { data: { key: KEY, envkeyfallback: false } } })
}
return Promise.resolve({ data: { data: {} } })
})
vi.stubGlobal('confirm', vi.fn(() => true))
vi.spyOn(console, 'error').mockImplementation(() => {})
const wrapper = mount(AgentKeySettings, { global: { stubs: { RouterLink: RouterLinkStub } } })
await flushPromises()
expect(wrapper.find('.enrollment-summary').exists()).toBe(false)
const button = wrapper.findAll('button').find(b => b.text() === 'Revoke key')
expect(button.exists()).toBe(true)
})
})