Some checks are pending
ci / 정본·보안·린트·타입·테스트 (push) Waiting to run
ci / 워크스페이스 빌드 검증 (push) Blocked by required conditions
ci / 모바일 린트·타입·Jest (push) Waiting to run
ci / Supabase Edge Functions + Cloudflare Worker (push) Waiting to run
ci / .NET API 서버 테스트 (push) Waiting to run
deploy-site / deploy (push) Waiting to run
CI run 121 failed at "버전·계약 정본 대조": earlier refactors moved update gating from UpdateService into update-policy.ts (evaluateUpdateOffer ties decideUpdate + isWithinRollout), the differential-download switch into update-adapters.ts, and the Forgejo re-release guard into lib/immutable-package-guard.mjs (sha256 comparison, abort on conflict). The checks still looked for the old markers in the old files. They now verify the same guarantees where the code lives, and the self-test's negative case targets assertNoImmutableConflicts. Also clears the lint gate: a control-character regex in Keycap (range now starts at U+0020) and unused helpers in two red-team tests.
86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
// tests/main/update-policy-redteam-r3-5.test.ts
|
|
// 원격 정책의 안전 필드(killSwitch, stagingPercentage)가 형식이 틀리면 가장 허용적인 값이
|
|
// 아니라 가장 보수적인 값으로 읽히는지 확인한다.
|
|
//
|
|
// 회귀: `"killSwitch": "true"` 가 false 로, `"stagingPercentage": "5"` / 5.5 가 100% 로
|
|
// 조용히 바뀌어 운영자 의도와 반대로 전체 배포되던 문제.
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
DEFAULT_UPDATE_POLICY,
|
|
evaluateUpdateOffer,
|
|
parseUpdatePolicy,
|
|
} from '../../src/main/update-policy'
|
|
|
|
const BASE = {
|
|
schemaVersion: 1,
|
|
defaultChannel: 'latest',
|
|
channels: {
|
|
latest: { allowPrerelease: false },
|
|
beta: { allowPrerelease: true },
|
|
alpha: { allowPrerelease: true },
|
|
},
|
|
minimumSupportedVersion: '1.0.0',
|
|
forceInstallBelow: null,
|
|
fullInstallOnMajorChange: true,
|
|
fullInstallVersionGap: 3,
|
|
stagingPercentage: 100,
|
|
killSwitch: false,
|
|
}
|
|
|
|
describe('parseUpdatePolicy — 안전 필드 fail-closed', () => {
|
|
it.each(['true', 'false', 1, 0, null, {}])('killSwitch=%j 는 켜진 것으로 본다', (value) => {
|
|
expect(parseUpdatePolicy({ ...BASE, killSwitch: value }).killSwitch).toBe(true)
|
|
})
|
|
|
|
it.each([5.5, '5', null, Number.NaN, '100'])('stagingPercentage=%j 는 0% 로 본다', (value) => {
|
|
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: value }).stagingPercentage).toBe(0)
|
|
})
|
|
|
|
it('형식이 틀린 킬 스위치는 업데이트 제안을 막는다', () => {
|
|
const policy = parseUpdatePolicy({ ...BASE, killSwitch: 'true' })
|
|
const offer = evaluateUpdateOffer({
|
|
policy,
|
|
channel: 'latest',
|
|
currentVersion: '1.8.0',
|
|
targetVersion: '1.9.0',
|
|
skippedVersion: null,
|
|
deviceId: 'device-a',
|
|
})
|
|
expect(offer).toMatchObject({ action: 'ignore', reason: 'kill-switch' })
|
|
})
|
|
|
|
it('형식이 틀린 staging 은 필수 업데이트가 아니면 아무에게도 노출하지 않는다', () => {
|
|
const policy = parseUpdatePolicy({ ...BASE, stagingPercentage: '5' })
|
|
for (const deviceId of ['a', 'b', 'c', 'd', 'e', 'f']) {
|
|
const offer = evaluateUpdateOffer({
|
|
policy,
|
|
channel: 'latest',
|
|
currentVersion: '1.8.0',
|
|
targetVersion: '1.9.0',
|
|
skippedVersion: null,
|
|
deviceId,
|
|
})
|
|
expect(offer).toMatchObject({ action: 'ignore', reason: 'rollout' })
|
|
}
|
|
})
|
|
|
|
it('필드가 아예 없으면 기존처럼 기본값을 쓴다', () => {
|
|
// 두 필드를 뺀 정책 — 나머지는 BASE 그대로
|
|
const rest = Object.fromEntries(
|
|
Object.entries(BASE).filter(([field]) => field !== 'killSwitch' && field !== 'stagingPercentage'),
|
|
)
|
|
const parsed = parseUpdatePolicy(rest)
|
|
expect(parsed.killSwitch).toBe(DEFAULT_UPDATE_POLICY.killSwitch)
|
|
expect(parsed.stagingPercentage).toBe(DEFAULT_UPDATE_POLICY.stagingPercentage)
|
|
})
|
|
|
|
it('올바른 값은 그대로(범위 밖 정수는 기존처럼 잘라서) 읽는다', () => {
|
|
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: 5, killSwitch: false })).toMatchObject({
|
|
stagingPercentage: 5,
|
|
killSwitch: false,
|
|
})
|
|
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: 250 }).stagingPercentage).toBe(100)
|
|
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: -3 }).stagingPercentage).toBe(0)
|
|
})
|
|
})
|