Skip to content

Commit 67d1c9b

Browse files
feat: add recommended test 6.2.40
1 parent 56ceccc commit 67d1c9b

File tree

6 files changed

+192
-2
lines changed

6 files changed

+192
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,6 @@ The following tests are not yet implemented and therefore missing:
360360
- Recommended Test 6.2.37
361361
- Recommended Test 6.2.38
362362
- Recommended Test 6.2.39
363-
- Recommended Test 6.2.40
364363
- Recommended Test 6.2.41
365364
- Recommended Test 6.2.42
366365
- Recommended Test 6.2.43
@@ -462,6 +461,7 @@ export const recommendedTest_6_2_16: DocumentTest
462461
export const recommendedTest_6_2_17: DocumentTest
463462
export const recommendedTest_6_2_18: DocumentTest
464463
export const recommendedTest_6_2_22: DocumentTest
464+
export const recommendedTest_6_2_40: DocumentTest
465465
```
466466
467467
[(back to top)](#bsi-csaf-validator-lib)

csaf_2_1/recommendedTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2
3131
export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js'
3232
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
3333
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
34+
export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import translations from '../../lib/language_specific_translation/translations.js'
3+
import bcp47 from 'bcp47'
4+
5+
const ajv = new Ajv()
6+
7+
const inputSchema = /** @type {const} */ ({
8+
additionalProperties: true,
9+
properties: {
10+
document: {
11+
additionalProperties: true,
12+
optionalProperties: {
13+
lang: { type: 'string' },
14+
notes: {
15+
elements: {
16+
additionalProperties: true,
17+
optionalProperties: {
18+
category: { type: 'string' },
19+
title: { type: 'string' },
20+
group_ids: { elements: { type: 'string' } },
21+
product_ids: { elements: { type: 'string' } },
22+
},
23+
},
24+
},
25+
},
26+
},
27+
},
28+
})
29+
30+
const validate = ajv.compile(inputSchema)
31+
32+
/**
33+
* Checks if the document language is English or unspecified
34+
*
35+
* @param {string | undefined} language The language expression to check
36+
* @returns {boolean} True if the language is English or unspecified, false otherwise
37+
*/
38+
export function isLangEnglishOrUnspecified(language) {
39+
return !language || bcp47.parse(language)?.langtag.language.language === 'en'
40+
}
41+
42+
/**
43+
* Get the language specific translation of the given i18nKey
44+
* @param {string } lang
45+
* @param {string} i18nKey
46+
* @returns {string | undefined} - The language specific translation of the `i18nKey`
47+
* - or undefined if the provided language could not be parsed as a BCP 47 tag
48+
* - or undefined if no translation of the `i18nKey` could be found
49+
*/
50+
export function getTranslationInDocumentLang(lang, i18nKey) {
51+
const language = bcp47.parse(lang)?.langtag.language.language
52+
53+
/** @type {Record<string, Record <string,string>>}*/
54+
const translationByLang = translations.translation
55+
56+
if (!language || !translationByLang[language]) {
57+
return undefined
58+
} else {
59+
return translationByLang[language][i18nKey]
60+
}
61+
}
62+
63+
/**
64+
* Check if the given note item contains at least one of `group_ids` or `product_ids`
65+
* @param {{ group_ids?: string[]; product_ids?: string[]}} note
66+
* @return {boolean}
67+
*/
68+
export function containsNoteGroupIdOrProductId(note) {
69+
return !!(note.group_ids || note.product_ids)
70+
}
71+
72+
/**
73+
* This implements the recommended test 6.2.40 of the CSAF 2.1 standard.
74+
*
75+
/**
76+
* @param {any} doc
77+
*/
78+
export function recommendedTest_6_2_40(doc) {
79+
/** @type {Array<{ message: string; instancePath: string }>} */
80+
const warnings = []
81+
const context = { warnings }
82+
83+
if (!validate(doc)) {
84+
return context
85+
}
86+
const documentLanguage = doc.document.lang
87+
doc.document.notes?.forEach((note, noteIndex) => {
88+
if (note.category === 'description') {
89+
if (isLangEnglishOrUnspecified(documentLanguage)) {
90+
if (note.title?.startsWith('Product Description')) {
91+
if (!containsNoteGroupIdOrProductId(note)) {
92+
context.warnings.push({
93+
instancePath: `/document/notes/${noteIndex}`,
94+
message:
95+
'The given note item must include one of the elments "group_id" or "prodcut_id"',
96+
})
97+
}
98+
}
99+
} else {
100+
const translation = getTranslationInDocumentLang(
101+
/** @type {string} */ (
102+
documentLanguage
103+
) /* This cast is allowed since the else statement is just called
104+
id documentLanguage is not undefined. Without the cast one must check here too
105+
if documentLanguage is not undefined, which would be a code fragment that is newer used*/,
106+
'product_description'
107+
)
108+
if (!translation) {
109+
//TODO: The warning is just a placeholder. The test should "...be skipped and a output should be shown to the
110+
// user with the text that no translation available...".
111+
// How this output should be implemented has to be clarified
112+
context.warnings.push({
113+
instancePath: `/document/notes/${noteIndex}`,
114+
message:
115+
'no language specific translation for the "title" of this note has been recorded',
116+
})
117+
return context
118+
}
119+
if (note.title?.startsWith(translation)) {
120+
if (!containsNoteGroupIdOrProductId(note)) {
121+
context.warnings.push({
122+
instancePath: `/document/notes/${noteIndex}`,
123+
message:
124+
'The given note item must include one of the elements "group_id" or "product_id"',
125+
})
126+
}
127+
}
128+
}
129+
}
130+
})
131+
return context
132+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default {
2+
$schema:
3+
'https://raw.githubusercontent.com/oasis-tcs/csaf/master/csaf_2.1/test/language_specific_translation/translations_json_schema.json',
4+
translation_version: '2.1',
5+
translation: {
6+
de: {
7+
license: 'Lizenz',
8+
product_description: 'Produktbeschreibung',
9+
reasoning_for_supersession: 'Begründung für die Ersetzung',
10+
reasoning_for_withdrawal: 'Begründung für die Zurückziehung',
11+
superseding_document: 'Ersetzendes Dokument',
12+
},
13+
},
14+
}

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const excluded = [
6060
'6.2.39.2',
6161
'6.2.39.3',
6262
'6.2.39.4',
63-
'6.2.40',
6463
'6.2.41',
6564
'6.2.42',
6665
'6.2.43',
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import assert from 'node:assert/strict'
2+
import { recommendedTest_6_2_40 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_40.js'
3+
4+
describe('recommendedTest_6_2_40', function () {
5+
it('only runs on relevant documents', function () {
6+
assert.equal(recommendedTest_6_2_40({}).warnings.length, 0)
7+
})
8+
it('skips empty objects', function () {
9+
assert.equal(
10+
recommendedTest_6_2_40({
11+
document: {
12+
notes: [
13+
{
14+
category: 'description',
15+
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.',
16+
title: 'Product Description',
17+
},
18+
{}, // skip this empty object
19+
],
20+
},
21+
}).warnings.length,
22+
1
23+
)
24+
})
25+
// 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
26+
it('no language specific translation', function () {
27+
assert.equal(
28+
recommendedTest_6_2_40({
29+
document: {
30+
lang: '123456789',
31+
notes: [
32+
{
33+
category: 'description',
34+
product_ids: ['CSAFPID-9080700'],
35+
text: 'Produkt A is ein lokales Zeiterfassungstool. Es wird hauptsächlich von Softwareentwicklern verwendet und kann an die meisten modernen Zeiterfasssungssysteme angebunden werden.',
36+
title: 'Produkt A wird hier beschrieben',
37+
},
38+
],
39+
},
40+
}).warnings.length,
41+
1
42+
)
43+
})
44+
})

0 commit comments

Comments
 (0)