Files
shopdb-flask/plugins/computers/frontend/views/pcDetailCredential.spec.js
cproudlock c2ffc591bb
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
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 14:09:08 -04:00

59 lines
2.7 KiB
JavaScript

/**
* Per-PC credential controls on PC Details.
*
* Revoke and allow-reenroll are admin-role routes on the server. The page reads
* the credential from the same endpoint state call the liveness chip uses, and
* offers the two actions to admins only; every other reader gets the state in
* the Endpoint status card, where the server has already redacted the details.
*
* Asserted against the file text, matching pcDetailRunScripts.spec.js.
*/
import { describe, it, expect } from 'vitest'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const here = dirname(fileURLToPath(import.meta.url))
const source = readFileSync(join(here, 'PCDetail.vue'), 'utf8')
// Relative to the staged copy under src/.plugins-staged/, which is what runs.
const api = readFileSync(join(here, '..', '..', '..', 'api', 'index.js'), 'utf8')
describe('PCDetail credential controls', () => {
it('gates the controls on the admin role', () => {
expect(source).toMatch(/const canManageCredential = computed\(\(\) => authStore\.isAdmin\)/)
expect(source).toMatch(/v-if="canManageCredential && credential"/)
})
it('reads the credential from the endpoint state response', () => {
const loader = source.slice(source.indexOf('async function loadLiveness'))
const body = loader.slice(0, loader.indexOf('\n}\n'))
expect(body).toMatch(/credential\.value = state\.credential \|\| null/)
})
it('offers revoke only for an enrolled PC and allow only for a blocked one', () => {
expect(source).toMatch(/v-if="credential\.state === 'enrolled'"[\s\S]{0,120}credential-revoke/)
expect(source).toMatch(/v-if="credential\.reenrollblocked"[\s\S]{0,120}credential-allow/)
})
it('offers the keep-from-enrolling option and sends it with the reason', () => {
expect(source).toMatch(/v-model="revokeblockreenroll"/)
expect(source).toMatch(/Keep this PC from enrolling again/)
const handler = source.slice(source.indexOf('async function submitRevokeCredential'))
const body = handler.slice(0, handler.indexOf('\n}\n'))
expect(body).toMatch(/blockreenroll: revokeblockreenroll\.value/)
expect(body).toMatch(/reason: revokereason\.value\.trim\(\)/)
})
it('calls the existing enrollment routes', () => {
expect(api).toMatch(/`\/endpoints\/enrollment\/assets\/\$\{assetId\}\/revoke`/)
expect(api).toMatch(/`\/endpoints\/enrollment\/assets\/\$\{assetId\}\/allow-reenroll`/)
expect(api).toMatch(/if \(opts\.blockreenroll\) \{\s*body\.blockreenroll = true/)
})
it('confirms before allowing re-enrollment', () => {
const handler = source.slice(source.indexOf('async function confirmAllowReenroll'))
const body = handler.slice(0, handler.indexOf('\n}\n'))
expect(body).toMatch(/if \(!confirm\(/)
})
})