From 62a69aabdefdf8bce15289d6c7ec8b29f2a9a876 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Wed, 30 Jul 2025 12:59:31 +0200 Subject: [PATCH 1/2] feat: add recommended test 6.2.40 --- README.md | 2 +- csaf_2_1/recommendedTests.js | 1 + .../recommendedTest_6_2_40.js | 132 ++++++++++++++++++ .../translations.js | 14 ++ tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/recommendedTest_6_2_40.js | 44 ++++++ 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_40.js create mode 100644 lib/language_specific_translation/translations.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_40.js diff --git a/README.md b/README.md index 6f4f9297..79754ea3 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.37 - Recommended Test 6.2.38 - Recommended Test 6.2.39 -- Recommended Test 6.2.40 - Recommended Test 6.2.41 - Recommended Test 6.2.42 - Recommended Test 6.2.43 @@ -461,6 +460,7 @@ export const recommendedTest_6_2_17: DocumentTest export const recommendedTest_6_2_18: DocumentTest export const recommendedTest_6_2_22: DocumentTest export const recommendedTest_6_2_23: DocumentTest +export const recommendedTest_6_2_40: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index a39c6673..41456776 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -32,3 +32,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2 export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js' export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js' export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js' +export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js new file mode 100644 index 00000000..e77f0132 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js @@ -0,0 +1,132 @@ +import Ajv from 'ajv/dist/jtd.js' +import translations from '../../lib/language_specific_translation/translations.js' +import bcp47 from 'bcp47' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + optionalProperties: { + lang: { type: 'string' }, + notes: { + elements: { + additionalProperties: true, + optionalProperties: { + category: { type: 'string' }, + title: { type: 'string' }, + group_ids: { elements: { type: 'string' } }, + product_ids: { elements: { type: 'string' } }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * Checks if the document language is English or unspecified + * + * @param {string | undefined} language The language expression to check + * @returns {boolean} True if the language is English or unspecified, false otherwise + */ +export function isLangEnglishOrUnspecified(language) { + return !language || bcp47.parse(language)?.langtag.language.language === 'en' +} + +/** + * Get the language specific translation of the given i18nKey + * @param {string } lang + * @param {string} i18nKey + * @returns {string | undefined} - The language specific translation of the `i18nKey` + * - or undefined if the provided language could not be parsed as a BCP 47 tag + * - or undefined if no translation of the `i18nKey` could be found + */ +export function getTranslationInDocumentLang(lang, i18nKey) { + const language = bcp47.parse(lang)?.langtag.language.language + + /** @type {Record>}*/ + const translationByLang = translations.translation + + if (!language || !translationByLang[language]) { + return undefined + } else { + return translationByLang[language][i18nKey] + } +} + +/** + * Check if the given note item contains at least one of `group_ids` or `product_ids` + * @param {{ group_ids?: string[]; product_ids?: string[]}} note + * @return {boolean} + */ +export function containsNoteGroupIdOrProductId(note) { + return !!(note.group_ids || note.product_ids) +} + +/** + * This implements the recommended test 6.2.40 of the CSAF 2.1 standard. + * + /** + * @param {any} doc + */ +export function recommendedTest_6_2_40(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const warnings = [] + const context = { warnings } + + if (!validate(doc)) { + return context + } + const documentLanguage = doc.document.lang + doc.document.notes?.forEach((note, noteIndex) => { + if (note.category === 'description') { + if (isLangEnglishOrUnspecified(documentLanguage)) { + if (note.title?.startsWith('Product Description')) { + if (!containsNoteGroupIdOrProductId(note)) { + context.warnings.push({ + instancePath: `/document/notes/${noteIndex}`, + message: + 'The given note item must include one of the elements "group_id" or "product_id"', + }) + } + } + } else { + const translation = getTranslationInDocumentLang( + /** @type {string} */ ( + documentLanguage + ) /* This cast is allowed since the else statement is just called + id documentLanguage is not undefined. Without the cast one must check here too + if documentLanguage is not undefined, which would be a code fragment that is newer used*/, + 'product_description' + ) + if (!translation) { + //TODO: The warning is just a placeholder. The test should "...be skipped and a output should be shown to the + // user with the text that no translation available...". + // How this output should be implemented has to be clarified + context.warnings.push({ + instancePath: `/document/notes/${noteIndex}`, + message: + 'no language specific translation for the "title" of this note has been recorded', + }) + return context + } + if (note.title?.startsWith(translation)) { + if (!containsNoteGroupIdOrProductId(note)) { + context.warnings.push({ + instancePath: `/document/notes/${noteIndex}`, + message: + 'The given note item must include one of the elements "group_id" or "product_id"', + }) + } + } + } + } + }) + return context +} diff --git a/lib/language_specific_translation/translations.js b/lib/language_specific_translation/translations.js new file mode 100644 index 00000000..9d9d7538 --- /dev/null +++ b/lib/language_specific_translation/translations.js @@ -0,0 +1,14 @@ +export default { + $schema: + 'https://raw.githubusercontent.com/oasis-tcs/csaf/master/csaf_2.1/test/language_specific_translation/translations_json_schema.json', + translation_version: '2.1', + translation: { + de: { + license: 'Lizenz', + product_description: 'Produktbeschreibung', + reasoning_for_supersession: 'Begründung für die Ersetzung', + reasoning_for_withdrawal: 'Begründung für die Zurückziehung', + superseding_document: 'Ersetzendes Dokument', + }, + }, +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index ba281650..9d8353d7 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -48,7 +48,6 @@ const excluded = [ '6.2.39.2', '6.2.39.3', '6.2.39.4', - '6.2.40', '6.2.41', '6.2.42', '6.2.43', diff --git a/tests/csaf_2_1/recommendedTest_6_2_40.js b/tests/csaf_2_1/recommendedTest_6_2_40.js new file mode 100644 index 00000000..7c0c5cdb --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_40.js @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import { recommendedTest_6_2_40 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_40.js' + +describe('recommendedTest_6_2_40', function () { + it('only runs on relevant documents', function () { + assert.equal(recommendedTest_6_2_40({}).warnings.length, 0) + }) + it('skips empty objects', function () { + assert.equal( + recommendedTest_6_2_40({ + document: { + notes: [ + { + category: 'description', + text: 'Product A is a local time tracking tool. It is mainly used by software developers and can be connected with most modern time-tracking systems.', + title: 'Product Description', + }, + {}, // skip this empty object + ], + }, + }).warnings.length, + 1 + ) + }) + // TODO: just a placeholder to get a proper code coverage until to-do in csaf_2_1/recommendedTests/recommendedTest_6_2_40.js is solved + it('no language specific translation', function () { + assert.equal( + recommendedTest_6_2_40({ + document: { + lang: '123456789', + notes: [ + { + category: 'description', + product_ids: ['CSAFPID-9080700'], + text: 'Produkt A is ein lokales Zeiterfassungstool. Es wird hauptsächlich von Softwareentwicklern verwendet und kann an die meisten modernen Zeiterfasssungssysteme angebunden werden.', + title: 'Produkt A wird hier beschrieben', + }, + ], + }, + }).warnings.length, + 1 + ) + }) +}) From 61d1d18901964a4751415d966f2dd38eb57e3fc9 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Tue, 9 Sep 2025 16:47:20 +0200 Subject: [PATCH 2/2] feat: adapt recommended test 6.2.40 --- .../recommendedTests/recommendedTest_6_2_40.js | 18 +++++++++--------- tests/csaf_2_1/recommendedTest_6_2_40.js | 3 +-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js index e77f0132..07f7ef40 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_40.js @@ -76,9 +76,12 @@ export function containsNoteGroupIdOrProductId(note) { * @param {any} doc */ export function recommendedTest_6_2_40(doc) { - /** @type {Array<{ message: string; instancePath: string }>} */ - const warnings = [] - const context = { warnings } + /** @type { {warnings: Array<{ message: string; instancePath: string }>; + * infos: Array<{ message: string; instancePath: string }>}} */ + const context = { + warnings: [], + infos: [], + } if (!validate(doc)) { return context @@ -92,7 +95,7 @@ export function recommendedTest_6_2_40(doc) { context.warnings.push({ instancePath: `/document/notes/${noteIndex}`, message: - 'The given note item must include one of the elements "group_id" or "product_id"', + 'the given note item must include one of the elements "group_id" or "product_id"', }) } } @@ -106,10 +109,7 @@ export function recommendedTest_6_2_40(doc) { 'product_description' ) if (!translation) { - //TODO: The warning is just a placeholder. The test should "...be skipped and a output should be shown to the - // user with the text that no translation available...". - // How this output should be implemented has to be clarified - context.warnings.push({ + context.infos.push({ instancePath: `/document/notes/${noteIndex}`, message: 'no language specific translation for the "title" of this note has been recorded', @@ -121,7 +121,7 @@ export function recommendedTest_6_2_40(doc) { context.warnings.push({ instancePath: `/document/notes/${noteIndex}`, message: - 'The given note item must include one of the elements "group_id" or "product_id"', + 'the given note item must include one of the elements "group_id" or "product_id"', }) } } diff --git a/tests/csaf_2_1/recommendedTest_6_2_40.js b/tests/csaf_2_1/recommendedTest_6_2_40.js index 7c0c5cdb..b691bf67 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_40.js +++ b/tests/csaf_2_1/recommendedTest_6_2_40.js @@ -22,7 +22,6 @@ describe('recommendedTest_6_2_40', function () { 1 ) }) - // TODO: just a placeholder to get a proper code coverage until to-do in csaf_2_1/recommendedTests/recommendedTest_6_2_40.js is solved it('no language specific translation', function () { assert.equal( recommendedTest_6_2_40({ @@ -37,7 +36,7 @@ describe('recommendedTest_6_2_40', function () { }, ], }, - }).warnings.length, + }).infos.length, 1 ) })