Skip to content
Open
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
2 changes: 1 addition & 1 deletion csaf_2_1/mandatoryTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export {
mandatoryTest_6_1_3,
mandatoryTest_6_1_5,
mandatoryTest_6_1_12,
mandatoryTest_6_1_14,
mandatoryTest_6_1_15,
mandatoryTest_6_1_16,
mandatoryTest_6_1_17,
Expand Down Expand Up @@ -38,6 +37,7 @@ export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
export { mandatoryTest_6_1_10 } from './mandatoryTests/mandatoryTest_6_1_10.js'
export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
export { mandatoryTest_6_1_14 } from './mandatoryTests/mandatoryTest_6_1_14.js'
export { mandatoryTest_6_1_27_3 } from './mandatoryTests/mandatoryTest_6_1_27_3.js'
export { mandatoryTest_6_1_27_4 } from './mandatoryTests/mandatoryTest_6_1_27_4.js'
export { mandatoryTest_6_1_27_5 } from './mandatoryTests/mandatoryTest_6_1_27_5.js'
Expand Down
110 changes: 110 additions & 0 deletions csaf_2_1/mandatoryTests/mandatoryTest_6_1_14.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import semver from 'semver'
import { Ajv } from 'ajv/dist/jtd.js'
import { compareZonedDateTimes } from '../dateHelper.js'

const { gt, valid } = semver

const ajv = new Ajv()

const revisionHistoryEntrySchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
date: { type: 'string' },
number: { type: 'string' },
},
})

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
document: {
additionalProperties: true,
optionalProperties: {
tracking: {
additionalProperties: true,
optionalProperties: {
revision_history: {
elements: revisionHistoryEntrySchema,
},
},
},
},
},
},
})

const validate = ajv.compile(inputSchema)

/**
* @typedef {import('ajv/dist/core.js').JTDDataType<typeof revisionHistoryEntrySchema>} RevisionHistoryEntry
*/

/**
* Maps `number` to a value comparable with semver. Integer version numbers
* are mapped to semantic versioning by appending `.0.0`.
*
* @param {string} number
* @returns {string | null}
*/
const toComparableSemver = (number) => {
if (valid(number)) return number
return /^\d+$/.test(number) ? `${number}.0.0` : null
}

/**
* @param {RevisionHistoryEntry} a
* @param {RevisionHistoryEntry} b
* @returns {number}
*/
const compareEntries = (a, b) => {
const dateComparison = compareZonedDateTimes(a.date, b.date)
if (dateComparison !== 0) {
return dateComparison
}

const aVersion = toComparableSemver(a.number)
const bVersion = toComparableSemver(b.number)
if (aVersion === null || bVersion === null) {
return 0
}
return semver.compare(aVersion, bVersion)
}

/**
* @param {unknown} doc
*/
export function mandatoryTest_6_1_14(doc) {
const ctx = {
errors:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
isValid: true,
}

if (
!validate(doc) ||
!Array.isArray(doc.document?.tracking?.revision_history)
) {
return ctx
}

const sortedNumbers = doc.document.tracking.revision_history
.slice()
.sort(compareEntries)
.map((entry) => toComparableSemver(entry.number))
.filter(/** @returns {n is string} */ (n) => n !== null)

const isAscending = sortedNumbers.every(
(number, index, all) => index === 0 || gt(number, all[index - 1])
)

if (!isAscending) {
ctx.isValid = false
ctx.errors.push({
instancePath: `/document/tracking/revision_history`,
message:
'items must be in ascending order when sorted by "date" and "number"',
})
}

return ctx
}
2 changes: 1 addition & 1 deletion lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface Result {
infos: Array<{ message: string; instancePath: string }>
}

interface TestResult {
export interface TestResult {
isValid?: boolean
warnings?: Array<{ message: string; instancePath: string }>
errors?: Array<{ message: string; instancePath: string }>
Expand Down
27 changes: 27 additions & 0 deletions tests/csaf_2_1/mandatoryTest_6_1_14.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict'

import { mandatoryTest_6_1_14 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_14.js'

describe('mandatoryTest_6_1_14', function () {
it('only runs on relevant documents', function () {
assert.equal(mandatoryTest_6_1_14({ product_tree: 'mydoc' }).isValid, true)
})

it('skips documents with invalid revision_history entries', function () {
const doc = {
document: {
tracking: {
revision_history: [
{ date: '2020-01-01T00:00:00+00:00', number: '1.0.0' },
{ date: '2020-01-01T00:00:00+00:00', number: 'invalid' },
],
},
},
}

assert.doesNotThrow(() => mandatoryTest_6_1_14(doc))
const result = mandatoryTest_6_1_14(doc)
assert.equal(result.isValid, true)
assert.deepEqual(result.errors, [])
})
})
34 changes: 20 additions & 14 deletions tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ const excluded = [
const skippedTests = new Set([
'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-03-01.json',
'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-03-02.json',
'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-14-32.json',
'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-21-17.json',
'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-27-08-02.json',
'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-11.json',
'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-12.json',
'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-13.json',
'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-02.json',
'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-01.json',
Comment on lines +84 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems not to belong here 🤔

])

/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */
Expand All @@ -106,9 +108,12 @@ const skippedTests = new Set([
/**
* @typedef {object} TestSpec
* @property {string} name
* @property {boolean} valid
* @property {boolean} valid // The valid field indicates whether the document is valid against all basic tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @property {boolean} valid // The valid field indicates whether the document is valid against all basic tests
* @property {boolean} valid - The valid field indicates whether the document is valid against all basic tests

*/

const TYPE_FAILURES = 'failures'
const TYPE_VALID = 'valid' //The entry in the array indicates that the test case is valid against the corresponding test.

const tests = new Map([
[
'informative',
Expand Down Expand Up @@ -145,24 +150,25 @@ for (const [group, t] of testMap) {
if (excluded.includes(testId)) continue

it(testSpec.name, async () => {
const test = tests
const testToExecute = tests
.get(group)
?.get(`${group}Test_${testId.replace(/\./g, '_')}`)

assert(test, 'test does not exist')
assert(testToExecute, 'test does not exist')

const doc = JSON.parse(
readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8')
)

const result = await test(doc)
const result = await testToExecute(doc)

if (group === 'mandatory') {
assert.equal(result.isValid, testSpec.valid)
const validForCurrentTest = type === TYPE_VALID
assert.equal(result.isValid, validForCurrentTest)
assert.equal(
Boolean(result.errors?.length),
type === 'failures',
type === 'failures'
type === TYPE_FAILURES,
type === TYPE_FAILURES
? 'should have errors'
: `should not have errors, but had ${result.errors?.length}`
)
Expand All @@ -172,16 +178,16 @@ for (const [group, t] of testMap) {
if (group === 'recommended') {
assert.equal(
Boolean(result.warnings?.length),
type === 'failures',
type === 'failures'
type === TYPE_FAILURES,
type === TYPE_FAILURES
? 'should have warnings'
: `should not have warnings, but had ${result.warnings?.length}`
)
} else if (group === 'informative') {
assert.equal(
Boolean(result.infos?.length),
type === 'failures',
type === 'failures'
type === TYPE_FAILURES,
type === TYPE_FAILURES
? 'should have infos'
: `should not have infos, but had ${result.infos?.length}`
)
Expand Down Expand Up @@ -216,8 +222,8 @@ function parseTestCases() {
new Map(testData.get(test.group)).set(
test.id,
new Map(testData.get(test.group)?.get(test.id))
.set('valid', valids)
.set('failures', failures)
.set(TYPE_VALID, valids)
.set(TYPE_FAILURES, failures)
)
)
}
Expand Down