Skip to content
Draft
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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ 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_39_2: DocumentTest
export const recommendedTest_6_2_39_3: DocumentTest
```

[(back to top)](#bsi-csaf-validator-lib)
Expand Down
2 changes: 2 additions & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ 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_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js'
export { recommendedTest_6_2_39_3 } from './recommendedTests/recommendedTest_6_2_39_3.js'
105 changes: 105 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 Withdrawal 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_2(doc) {
/*
The `ctx` variable holds the state that is accumulated during the test run and is
finally returned by the function.
*/
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}

const noteCategory = 'description'

if (!validateSchema(doc) || doc.document.category !== 'csaf_withdrawn') {
return ctx
}

const withdrawalInDocLang = getTranslationInDocumentLang(
doc,
'reasoning_for_withdrawal'
)
if (!withdrawalInDocLang) {
ctx.warnings.push({
instancePath: '/document/notes',
message:
'no language specific translation for "Reasoning for Withdrawal" has been recorded',
})
return ctx
}

if (isLangSpecifiedAndNotEnglish(doc.document.lang)) {
const notes = doc.document.notes
if (
!notes ||
!containsOneNoteWithTitleAndCategory(
notes,
withdrawalInDocLang,
noteCategory
)
) {
ctx.warnings.push({
instancePath: '/document/notes',
message:
`for document category "csaf_withdrawn" exactly one note must exist ` +
`with note category "${noteCategory}" and title "${withdrawalInDocLang}`,
})
}
}

return ctx
}
105 changes: 105 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_39_3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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.
*/
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}

const noteCategory = 'description'

if (!validateSchema(doc) || doc.document.category !== 'csaf_superseded') {
return ctx
}

const supersessionInDocLang = getTranslationInDocumentLang(
doc,
'reasoning_for_supersession'
)
if (!supersessionInDocLang) {
ctx.warnings.push({
instancePath: '/document/notes',
message:
'no language specific translation for "Reasoning for Supersession" has been recorded',
})
return ctx
}

if (isLangSpecifiedAndNotEnglish(doc.document.lang)) {
const notes = doc.document.notes
if (
!notes ||
!containsOneNoteWithTitleAndCategory(
notes,
supersessionInDocLang,
noteCategory
)
) {
ctx.warnings.push({
instancePath: '/document/notes',
message:
`for document category "csaf_withdrawn" exactly one note must exist ` +
`with note category "${noteCategory}" and title "${supersessionInDocLang}`,
})
}
}

return ctx
}
17 changes: 17 additions & 0 deletions lib/language_specific_translation/translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* JavaScript version of JSON file: csaf_2.1/language_specific_translation/translations.json
*/
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',
},
},
}
54 changes: 54 additions & 0 deletions lib/shared/languageSpecificTranslation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Checks if the document language is specified and not English
*
* @param {string | undefined} language - The language expression to check
* @returns {boolean} True if the language is valid, false otherwise
*/
export function isLangSpecifiedAndNotEnglish(language) {
return (
!!language && !(bcp47.parse(language)?.langtag.language.language === 'en')
)
}
import bcp47 from 'bcp47'
import translations from '../../lib/language_specific_translation/translations.js'

/**
* test whether exactly one item in document notes exists that has the given title.
* and the given category.
* @param {({} & { category?: string | undefined; title?: string | undefined; } & Record<string, unknown>)[]} notes
* @param {string} titleToFind
* @param {string} category
* @returns {boolean} True if the language is valid, false otherwise
*/
export function containsOneNoteWithTitleAndCategory(
notes,
titleToFind,
category
) {
return (
notes.filter(
(note) => note.category === category && note.title === titleToFind
).length === 1
)
}

/**
* Get the language specific translation of the given i18nKey
* @param {{ document: { lang?: string; }; }} doc
* @param {string} i18nKey
* @return {string | undefined}
*/
export function getTranslationInDocumentLang(doc, i18nKey) {
if (!doc.document.lang) {
return undefined
}
const language = bcp47.parse(doc.document.lang)?.langtag.language.language

/** @type {Record<string, Record <string,string>>}*/
const translationByLang = translations.translation
if (!language || !translationByLang[language]) {
return undefined
} else {
return translationByLang[language][i18nKey]
}
}
2 changes: 0 additions & 2 deletions tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ const excluded = [
'6.2.36',
'6.2.37',
'6.2.39.1',
'6.2.39.2',
'6.2.39.3',
'6.2.39.4',
'6.2.40',
'6.2.41',
Expand Down
37 changes: 37 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_39_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { recommendedTest_6_2_39_2 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js'
import { expect } from 'chai'
import assert from 'node:assert'
import { getTranslationInDocumentLang } from '../../lib/shared/languageSpecificTranslation.js'

describe('recommendedTest_6_2_39_2', function () {
it('only runs on relevant documents', function () {
assert.equal(recommendedTest_6_2_39_2({}).warnings.length, 0)
})

it('only runs on valid language', function () {
assert.equal(
recommendedTest_6_2_39_2({
document: { lang: '123', license_expression: 'MIT' },
}).warnings.length,
0
)
})

it('check get ReasoningForWithdrawal in document lang', function () {
expect(
getTranslationInDocumentLang(
{ document: { lang: 'de' } },
'reasoning_for_withdrawal'
)
).to.eq('Begründung für die Zurückziehung')
expect(
getTranslationInDocumentLang(
{ document: { lang: 'es' } },
'reasoning_for_withdrawal'
)
).to.eq(undefined)
expect(
getTranslationInDocumentLang({ document: {} }, 'reasoning_for_withdrawal')
).to.eq(undefined)
})
})
40 changes: 40 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_39_3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 language', function () {
assert.equal(
recommendedTest_6_2_39_3({
document: { lang: '123', license_expression: 'MIT' },
}).warnings.length,
0
)
})

it('check get ReasoningForWithdrawal 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)
})
})