diff --git a/README.md b/README.md index 8ed48733..dbcdfdcb 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,7 @@ export const recommendedTest_6_2_28: DocumentTest export const recommendedTest_6_2_29: DocumentTest export const recommendedTest_6_2_30: DocumentTest export const recommendedTest_6_2_39_2: DocumentTest +export const recommendedTest_6_2_39_3: DocumentTest export const recommendedTest_6_2_39_4: DocumentTest export const recommendedTest_6_2_40: DocumentTest export const recommendedTest_6_2_41: DocumentTest diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 7be8846f..46213ccb 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -35,6 +35,7 @@ export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_2 export { recommendedTest_6_2_30 } from './recommendedTests/recommendedTest_6_2_30.js' export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js' export { recommendedTest_6_2_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js' +export { recommendedTest_6_2_39_3 } from './recommendedTests/recommendedTest_6_2_39_3.js' export { recommendedTest_6_2_39_4 } from './recommendedTests/recommendedTest_6_2_39_4.js' export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js' export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js index 8aa07d2d..da6e3955 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js @@ -66,25 +66,29 @@ export function recommendedTest_6_2_39_2(doc) { } const noteCategory = 'description' + const docCategoryCsafWithdrawn = `csaf_withdrawn` - if (!validateSchema(doc) || doc.document.category !== 'csaf_withdrawn') { - return ctx - } - - const withdrawalInDocLang = getTranslationInDocumentLang( - doc, - 'reasoning_for_withdrawal' - ) - if (!withdrawalInDocLang) { - ctx.infos.push({ - instancePath: '/document/notes', - message: - 'no language specific translation for "Reasoning for Withdrawal" has been recorded', - }) + if ( + !validateSchema(doc) || + doc.document.category !== docCategoryCsafWithdrawn + ) { return ctx } if (isLangSpecifiedAndNotEnglish(doc.document.lang)) { + const withdrawalInDocLang = getTranslationInDocumentLang( + doc, + 'reasoning_for_withdrawal' + ) + if (!withdrawalInDocLang) { + ctx.infos.push({ + instancePath: '/document/notes', + message: + 'no language specific translation for "Reasoning for Withdrawal" has been recorded', + }) + return ctx + } + const notes = doc.document.notes if ( !notes || @@ -97,7 +101,7 @@ export function recommendedTest_6_2_39_2(doc) { ctx.warnings.push({ instancePath: '/document/notes', message: - `for document category "csaf_withdrawn" exactly one note must exist ` + + `for document category "${docCategoryCsafWithdrawn}" exactly one note must exist ` + `with note category "${noteCategory}" and title "${withdrawalInDocLang}"`, }) } diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_39_3.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_39_3.js new file mode 100644 index 00000000..b468b2ff --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_39_3.js @@ -0,0 +1,111 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { + containsOneNoteWithTitleAndCategory, + getTranslationInDocumentLang, + isLangSpecifiedAndNotEnglish, +} from '../../lib/shared/languageSpecificTranslation.js' + +const ajv = new Ajv() + +/* + This is the jtd schema that needs to match the input document so that the + test is activated. If this schema doesn't match it normally means that the input + document does not validate against the csaf json schema or optional fields that + the test checks are not present. + */ +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + category: { type: 'string' }, + }, + optionalProperties: { + lang: { + type: 'string', + }, + notes: { + elements: { + additionalProperties: true, + optionalProperties: { + category: { + type: 'string', + }, + title: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, +}) + +const validateSchema = ajv.compile(inputSchema) + +/** + * If the document language is specified but not English, it MUST be tested that exactly one item + * in document notes exists that has the language specific translation of the term Reasoning for Supersession as title, + * The category of this item MUST be description. If no language specific translation has been recorded, + * the test MUST be skipped and output an information to the user that no such translation is known. + * + * @param {unknown} doc + */ +export function recommendedTest_6_2_39_3(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test run and is + finally returned by the function. + */ + /** @type { {warnings: Array<{ message: string; instancePath: string }>; + * infos: Array<{ message: string; instancePath: string }>}} */ + const ctx = { + warnings: [], + infos: [], + } + + const noteCategory = 'description' + const docCategoryCsafSuperseded = `csaf_superseded` + + if ( + !validateSchema(doc) || + doc.document.category !== docCategoryCsafSuperseded + ) { + return ctx + } + + if (isLangSpecifiedAndNotEnglish(doc.document.lang)) { + const supersessionInDocLang = getTranslationInDocumentLang( + doc, + 'reasoning_for_supersession' + ) + if (!supersessionInDocLang) { + ctx.infos.push({ + instancePath: '/document/notes', + message: + 'no language specific translation for "Reasoning for Supersession" has been recorded', + }) + return ctx + } + + const notes = doc.document.notes + if ( + !notes || + !containsOneNoteWithTitleAndCategory( + notes, + supersessionInDocLang, + noteCategory + ) + ) { + ctx.warnings.push({ + instancePath: '/document/notes', + message: + `for document category "${docCategoryCsafSuperseded}" exactly one note must exist ` + + `with note category "${noteCategory}" and title "${supersessionInDocLang}"`, + }) + } + } + + return ctx +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 534a078e..dd9e5afd 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -47,7 +47,6 @@ const excluded = [ '6.2.36', '6.2.37', '6.2.39.1', - '6.2.39.3', '6.2.39.5', '6.2.42', '6.2.44', diff --git a/tests/csaf_2_1/recommendedTest_6_2_39_3.js b/tests/csaf_2_1/recommendedTest_6_2_39_3.js new file mode 100644 index 00000000..192babaa --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_39_3.js @@ -0,0 +1,52 @@ +import { recommendedTest_6_2_39_3 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_39_3.js' +import { expect } from 'chai' +import assert from 'node:assert' +import { getTranslationInDocumentLang } from '../../lib/shared/languageSpecificTranslation.js' + +describe('recommendedTest_6_2_39_3', function () { + it('only runs on relevant documents', function () { + assert.equal(recommendedTest_6_2_39_3({}).warnings.length, 0) + }) + + it('only runs on valid category', function () { + const result = recommendedTest_6_2_39_3({ + document: { category: '123', license_expression: 'MIT' }, + }) + + assert.equal(result.warnings.length, 0) + assert.equal(result.infos.length, 0) + }) + + it('info on invalid language', function () { + const result = recommendedTest_6_2_39_3({ + document: { + category: 'csaf_superseded', + lang: '123', + license_expression: 'MIT', + }, + }) + assert.equal(result.warnings.length, 0) + assert.equal(result.infos.length, 1) + }) + + it('check get reasoning_for_supersession in document lang', function () { + expect( + getTranslationInDocumentLang( + { document: { lang: 'de' } }, + 'reasoning_for_supersession' + ) + ).to.eq('Begründung für die Ersetzung') + expect( + getTranslationInDocumentLang( + { document: { lang: 'jp' } }, + 'reasoning_for_supersession' + ) + ).to.eq(undefined) + expect( + getTranslationInDocumentLang( + { document: {} }, + 'reasoning_for_supersession' + ) + ).to.eq(undefined) + }) +})