- Settings > Endpoints > Enrollment. Migration tab: readiness counts and progress, each PC's state with the reasons it is blocked, "Upgrade selected" and "Upgrade next 25", and live wave progress. Credentials tab: enrolled PCs with revoke, pending requests with approve and reject, and the shared key and auto-enroll switches. Turning the shared key off asks for confirmation and shows how many PCs are not enrolled. - The server certificate page shows the SPKI pin to put in the GE-Enforce pin entry. - The agent upgrade script and the asset and printer reporters send the PC's credential when it has one (over https, only to the issuing server), fall back to the shared key, check server pins, and log refusals clearly. - New Set-ShopDBServerPins.ps1 (GE-Enforce entry) and Get-ShopDBServerPin.ps1. - AGENT-CONTRACT.md documents per-PC credentials; the rollout runbook covers phases, gates, rollback, pin and wave entries, and every place the shared key lives and how to clear it at cutover. - Removed the 09-14 certificate design notes and dated session reports, and two VM notes that carried a live collector key and advised plain http. - Corrected the GE-Enforce collector note, which claimed a plain registry token is protected by ACL.
201 lines
7.7 KiB
JavaScript
201 lines
7.7 KiB
JavaScript
/**
|
|
* Enrollment settings page (per-PC credentials).
|
|
*
|
|
* Pins the wire shapes the page sends, because the server contract is exact:
|
|
* a wave is {"next": n} or {"assetids": [...]}, an approval carries the chosen
|
|
* assetid, and turning the shared key off must tell the admin how many PCs are
|
|
* not enrolled yet before anything is sent.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
|
|
vi.mock('@/api', () => ({
|
|
default: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
|
|
computersApi: { list: vi.fn() }
|
|
}))
|
|
vi.mock('@/stores/auth', () => ({
|
|
useAuthStore: () => ({ isAdmin: true, hasPermission: () => true })
|
|
}))
|
|
vi.mock('@/composables/toast', () => ({
|
|
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn() })
|
|
}))
|
|
vi.mock('@/components/PaginationBar.vue', () => ({
|
|
default: { name: 'PaginationBar', template: '<div />' }
|
|
}))
|
|
|
|
import api, { computersApi } from '@/api'
|
|
import EnrollmentSettings from './EnrollmentSettings.vue'
|
|
|
|
function migrationPayload() {
|
|
return {
|
|
data: {
|
|
data: {
|
|
target: '1.20.0',
|
|
counts: { enrolled: 40, pending: 2, upgrading: 3, ready: 30, blocked: 5, notreporting: 20 },
|
|
computers: [
|
|
{ assetid: 11, hostname: 'PC-READY', agentversion: '1.19.2', lastreportedat: null, state: 'ready', blockers: [] },
|
|
{ assetid: 12, hostname: 'PC-BLOCKED', agentversion: '1.18.0', lastreportedat: null, state: 'blocked',
|
|
blockers: ['offline', 'no serial on record'] }
|
|
],
|
|
waves: [
|
|
{ waveid: 3, createdat: null, createdby: 'admin', size: 25, queued: 25, enrolled: 10, failed: 1, done: false }
|
|
]
|
|
},
|
|
meta: { pagination: { totalpages: 1 } }
|
|
}
|
|
}
|
|
}
|
|
|
|
function enrollmentPayload() {
|
|
return {
|
|
data: {
|
|
data: {
|
|
settings: { sharedkeyallowed: true, autoenroll: true },
|
|
counts: { enrolled: 40, notenrolled: 58, pending: 2 },
|
|
computers: [
|
|
{ assetid: 21, hostname: 'PC-ENROLLED', serialnumber: 'S1', state: 'enrolled',
|
|
credentialprefix: 'sdbe_abcdefg', issuedat: null, lastusedat: null, enrolledvia: 'auto' }
|
|
],
|
|
requests: [
|
|
{ requestid: 45, hostname: 'PC-NEW', serialnumber: 'S9', deviceid: null, reason: 'serial mismatch',
|
|
status: 'pending', matchedassetid: 77, requestedat: null, lastseenat: null, sourceip: null }
|
|
]
|
|
},
|
|
meta: { pagination: { totalpages: 1 } }
|
|
}
|
|
}
|
|
}
|
|
|
|
async function render() {
|
|
api.get.mockImplementation(path => Promise.resolve(
|
|
path === '/endpoints/enrollment/migration' ? migrationPayload() : enrollmentPayload()))
|
|
api.post.mockResolvedValue({ data: { data: { waveid: 4, queued: [11], skipped: [] } } })
|
|
api.put.mockResolvedValue({ data: { data: {} } })
|
|
const wrapper = mount(EnrollmentSettings)
|
|
await flushPromises()
|
|
return wrapper
|
|
}
|
|
|
|
async function openCredentials(wrapper) {
|
|
const tab = wrapper.findAll('.enroll-tab').find(button => button.text() === 'Credentials')
|
|
await tab.trigger('click')
|
|
await flushPromises()
|
|
}
|
|
|
|
describe('EnrollmentSettings', () => {
|
|
beforeEach(() => {
|
|
api.get.mockReset()
|
|
api.post.mockReset()
|
|
api.put.mockReset()
|
|
computersApi.list.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('renders the migration counts and enrolled progress', async () => {
|
|
const wrapper = await render()
|
|
expect(wrapper.find('[data-count="enrolled"]').text()).toBe('40')
|
|
expect(wrapper.find('[data-count="ready"]').text()).toBe('30')
|
|
expect(wrapper.find('[data-count="blocked"]').text()).toBe('5')
|
|
expect(wrapper.find('[data-count="notreporting"]').text()).toBe('20')
|
|
expect(wrapper.find('.progress-text').text()).toContain('40 of 100 enrolled')
|
|
})
|
|
|
|
it('shows every blocker for a blocked PC', async () => {
|
|
const wrapper = await render()
|
|
const row = wrapper.findAll('.migration-row').find(r => r.text().includes('PC-BLOCKED'))
|
|
expect(row.find('.blockers').text()).toBe('offline, no serial on record')
|
|
})
|
|
|
|
it('only offers a checkbox on ready rows', async () => {
|
|
const wrapper = await render()
|
|
const rows = wrapper.findAll('.migration-row')
|
|
expect(rows[0].find('input[type="checkbox"]').exists()).toBe(true)
|
|
expect(rows[1].find('input[type="checkbox"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('posts {"next":25} for Upgrade next 25 after a confirm naming the count', async () => {
|
|
const confirmSpy = vi.fn(() => true)
|
|
vi.stubGlobal('confirm', confirmSpy)
|
|
const wrapper = await render()
|
|
await wrapper.find('.upgrade-next').trigger('click')
|
|
await flushPromises()
|
|
expect(confirmSpy).toHaveBeenCalledTimes(1)
|
|
expect(confirmSpy.mock.calls[0][0]).toMatch(/Upgrade 25 PC\(s\) to endpoint agent 1\.20\.0/)
|
|
expect(confirmSpy.mock.calls[0][0]).toMatch(/enrolls automatically/)
|
|
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/migration/wave', { next: 25 })
|
|
})
|
|
|
|
it('sends nothing when the wave confirm is declined', async () => {
|
|
vi.stubGlobal('confirm', vi.fn(() => false))
|
|
const wrapper = await render()
|
|
await wrapper.find('.upgrade-next').trigger('click')
|
|
await flushPromises()
|
|
expect(api.post).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('posts the selected assetids for Upgrade selected', async () => {
|
|
vi.stubGlobal('confirm', vi.fn(() => true))
|
|
const wrapper = await render()
|
|
await wrapper.findAll('.migration-row')[0].find('input[type="checkbox"]').setValue(true)
|
|
await wrapper.find('.upgrade-selected').trigger('click')
|
|
await flushPromises()
|
|
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/migration/wave', { assetids: [11] })
|
|
})
|
|
|
|
it('approves a request with the matched asset by default', async () => {
|
|
vi.stubGlobal('confirm', vi.fn(() => true))
|
|
const wrapper = await render()
|
|
await openCredentials(wrapper)
|
|
await wrapper.find('.approve-open').trigger('click')
|
|
expect(wrapper.find('.asset-picker').element.value).toBe('77')
|
|
await wrapper.find('.approve-confirm').trigger('click')
|
|
await flushPromises()
|
|
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/requests/45/approve', { assetid: 77 })
|
|
})
|
|
|
|
it('rejects a request after confirm', async () => {
|
|
vi.stubGlobal('confirm', vi.fn(() => true))
|
|
const wrapper = await render()
|
|
await openCredentials(wrapper)
|
|
await wrapper.find('.reject').trigger('click')
|
|
await flushPromises()
|
|
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/requests/45/reject')
|
|
})
|
|
|
|
it('un-enrolls through the revoke route', async () => {
|
|
vi.stubGlobal('confirm', vi.fn(() => true))
|
|
const wrapper = await render()
|
|
await openCredentials(wrapper)
|
|
await wrapper.find('.revoke').trigger('click')
|
|
await flushPromises()
|
|
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/assets/21/revoke')
|
|
})
|
|
|
|
it('shows the not-enrolled count before turning the shared key off', async () => {
|
|
const promptSpy = vi.fn(() => 'OFF')
|
|
vi.stubGlobal('prompt', promptSpy)
|
|
const wrapper = await render()
|
|
await openCredentials(wrapper)
|
|
await wrapper.find('.toggle-sharedkey').setValue(false)
|
|
await flushPromises()
|
|
expect(promptSpy).toHaveBeenCalledTimes(1)
|
|
expect(promptSpy.mock.calls[0][0]).toMatch(/58 PC\(s\) are not enrolled yet/)
|
|
expect(api.put).toHaveBeenCalledWith('/endpoints/enrollment/settings',
|
|
{ sharedkeyallowed: false, autoenroll: true })
|
|
})
|
|
|
|
it('leaves the shared key on when the confirmation is not typed', async () => {
|
|
vi.stubGlobal('prompt', vi.fn(() => 'yes'))
|
|
const wrapper = await render()
|
|
await openCredentials(wrapper)
|
|
const toggle = wrapper.find('.toggle-sharedkey')
|
|
await toggle.setValue(false)
|
|
await flushPromises()
|
|
expect(api.put).not.toHaveBeenCalled()
|
|
expect(toggle.element.checked).toBe(true)
|
|
})
|
|
})
|