Skip to content

Commit cd12da7

Browse files
committed
feat: add informative test 6.3.16
1 parent bd47845 commit cd12da7

File tree

10 files changed

+272
-17
lines changed

10 files changed

+272
-17
lines changed

.github/workflows/run-tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ jobs:
2222
- uses: actions/setup-node@v4
2323
with:
2424
node-version: ${{ matrix.node-version }}
25+
- name: Start language tool
26+
run: docker compose up -d
27+
- name: Wait for app start
28+
uses: ifaxity/wait-on-action@v1
29+
with:
30+
delay: 1
31+
timeout: 30000
32+
resource: tcp:localhost:8010
2533
- run: npm ci
2634
- run: npm run test-report
2735
- run: npm run test-coverage-lcov

DEVELOPMENT.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
## Table of Contents
44

5+
- [Language Tool](#language-tool)
56
- [Code Style](#code-style)
67
- [Formatting with prettier](#formatting-with-prettier)
78
- [Quoting Strings](#quoting-strings)
89

10+
## Language Tool
11+
12+
The informative test 6.3.16 needs a running languagetool server. To set one for development you can use the `compose.yml` provided with the repository:
13+
14+
docker compose up -d
15+
916
## Code Style
1017

1118
### Formatting with prettier

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,10 @@ The following tests are not yet implemented and therefore missing:
359359
360360
**Informative Tests**
361361
362-
- Informative Test 6.2.13
363-
- Informative Test 6.2.14
364-
- Informative Test 6.2.15
365-
- Informative Test 6.2.16
366-
- Informative Test 6.2.17
362+
- Informative Test 6.3.13
363+
- Informative Test 6.3.14
364+
- Informative Test 6.3.15
365+
- Informative Test 6.3.17
367366
368367
#### Module `csaf_2_1/schemaTests.js`
369368
@@ -480,6 +479,7 @@ export const informativeTest_6_3_9: DocumentTest
480479
export const informativeTest_6_3_10: DocumentTest
481480
export const informativeTest_6_3_11: DocumentTest
482481
export const informativeTest_6_3_12: DocumentTest
482+
export const informativeTest_6_3_16: DocumentTest
483483
```
484484
485485
[(back to top)](#bsi-csaf-validator-lib)

compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
languagetool:
3+
image: collabora/languagetool
4+
ports:
5+
- 8010:8010

context.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @typedef {object} Context
3+
* @property {string} languageToolUrl The url to the language tool
4+
*/
5+
6+
/**
7+
* This is the context that is used to execute the tests. Modify it when
8+
* initializing the library to change settings.
9+
*
10+
* @type {Context}
11+
*/
12+
export const context = { languageToolUrl: 'http://localhost:8010' }

csaf_2_1/informativeTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1.
1212
export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js'
1313
export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js'
1414
export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js'
15+
export { informativeTest_6_3_16 } from './informativeTests/informativeTest_6_3_16.js'
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
This test depends on the languagetool server to be available. See
3+
https://languagetool.org/de. A `compose.yml` file is available in the
4+
repository root to start an instance.
5+
*/
6+
7+
import Ajv from 'ajv/dist/jtd.js'
8+
import bcp47 from 'bcp47'
9+
import { context } from '../../context.js'
10+
11+
const ajv = new Ajv()
12+
13+
const inputSchema = /** @type {const} */ ({
14+
additionalProperties: true,
15+
optionalProperties: {
16+
document: {
17+
additionalProperties: true,
18+
optionalProperties: {
19+
lang: { type: 'string' },
20+
},
21+
},
22+
},
23+
})
24+
25+
const validateInput = ajv.compile(inputSchema)
26+
27+
/**
28+
* If the document language is given it MUST be tested that a grammar check for
29+
* the given language does not find any mistakes. The test SHALL be skipped if
30+
* the document language is not set. It SHALL fail if the given language is not
31+
* supported.
32+
*
33+
* @param {unknown} doc
34+
* @returns
35+
*/
36+
export async function informativeTest_6_3_16(doc) {
37+
const ctx = {
38+
infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]),
39+
}
40+
41+
if (!validateInput(doc)) {
42+
return ctx
43+
}
44+
45+
const lang =
46+
(doc.document?.lang &&
47+
bcp47.parse(doc.document.lang)?.langtag.language.language) ??
48+
'en'
49+
50+
/*
51+
Check if the language is supported by the languagetool server.
52+
*/
53+
{
54+
/**
55+
* @typedef {object} Language
56+
* @property {string} code
57+
*/
58+
59+
/** @typedef {Language[]} Response */
60+
61+
const res = await fetch(new URL('/v2/languages', context.languageToolUrl), {
62+
headers: {
63+
accept: 'application/json',
64+
},
65+
})
66+
if (!res.ok) throw new Error('request to languagetool failed')
67+
68+
const json = /** @type {Response} */ (await res.json())
69+
70+
if (!json.some((l) => l.code === lang)) {
71+
ctx.infos.push({
72+
instancePath: '/document/lang',
73+
message: 'language is not supported',
74+
})
75+
}
76+
}
77+
78+
for (const path of [
79+
'/document/acknowledgments[]/summary',
80+
'/document/aggregate_severity/text',
81+
'/document/distribution/text',
82+
'/document/notes[]/audience',
83+
'/document/notes[]/text',
84+
'/document/notes[]/title',
85+
'/document/publisher/issuing_authority',
86+
'/document/references[]/summary',
87+
'/document/title',
88+
'/document/tracking/revision_history[]/summary',
89+
'/product_tree/product_groups[]/summary',
90+
'/vulnerabilities[]/acknowledgments[]/summary',
91+
'/vulnerabilities[]/involvements[]/summary',
92+
'/vulnerabilities[]/notes[]/audience',
93+
'/vulnerabilities[]/notes[]/text',
94+
'/vulnerabilities[]/notes[]/title',
95+
'/vulnerabilities[]/references[]/summary',
96+
'/vulnerabilities[]/remediations[]/details',
97+
'/vulnerabilities[]/remediations[]/entitlements[]',
98+
'/vulnerabilities[]/remediations[]/restart_required/details',
99+
'/vulnerabilities[]/threats[]/details',
100+
'/vulnerabilities[]/title',
101+
]) {
102+
await checkPath(
103+
[],
104+
path.split('/').slice(1),
105+
doc,
106+
async (instancePath, text) => {
107+
if (typeof text !== 'string') return
108+
const result = await checkString(text, lang)
109+
if (result.length) {
110+
ctx.infos.push({
111+
instancePath,
112+
message: result.map((r) => r.message).join(' '),
113+
})
114+
}
115+
}
116+
)
117+
}
118+
119+
return ctx
120+
}
121+
122+
/**
123+
* Checks the value behind `path` using the given `onCheck` function. This is a
124+
* recursive helper function to loop through the list of paths in the spec.
125+
*
126+
* @param {string[]} reminder
127+
* @param {string[]} path
128+
* @param {unknown} value
129+
* @param {(instancePath: string, value: string) => Promise<void>} onCheck
130+
*/
131+
async function checkPath(reminder, path, value, onCheck) {
132+
if (value == null) return
133+
const currentSegment = path.at(0)
134+
135+
if (!currentSegment) {
136+
// We've reached the end. Now the `onCheck` function can be called to check
137+
// the actual value.
138+
if (typeof value === 'string') {
139+
await onCheck('/' + reminder.join('/'), value)
140+
}
141+
} else if (currentSegment.endsWith('[]')) {
142+
// The value is supposed to be an array for which every element needs to be
143+
// checked ...
144+
const arrayName = currentSegment.split('[')[0]
145+
const array = Reflect.get(value, arrayName)
146+
147+
if (Array.isArray(array)) {
148+
// ... But only if it's really an array.
149+
for (const [elementIndex, element] of array.entries() ?? []) {
150+
await checkPath(
151+
[...reminder, arrayName, String(elementIndex)],
152+
[...path.slice(1)],
153+
element,
154+
onCheck
155+
)
156+
}
157+
}
158+
} else {
159+
// Otherwise it's something object-ish which we traverse recursively.
160+
await checkPath(
161+
[...reminder, currentSegment],
162+
path.slice(1),
163+
Reflect.get(value, currentSegment),
164+
onCheck
165+
)
166+
}
167+
}
168+
169+
/**
170+
* Check the given string using the languagetool server.
171+
*
172+
* @param {string} str
173+
* @param {string} lng
174+
* @returns
175+
*/
176+
async function checkString(str, lng) {
177+
/**
178+
* @typedef {object} Match
179+
* @property {string} message
180+
*/
181+
182+
/**
183+
* @typedef {object} Response
184+
* @property {Match[]} matches
185+
*/
186+
187+
const res = await fetch(new URL('/v2/check', context.languageToolUrl), {
188+
method: 'POST',
189+
body: new URLSearchParams([
190+
['language', lng],
191+
['text', str],
192+
]),
193+
})
194+
if (!res.ok) throw new Error('request to languagetool failed')
195+
196+
const json = /** @type {Response} */ (await res.json())
197+
return json.matches
198+
}

scripts/test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
import { spawn } from 'child_process'
44
import { fileURLToPath } from 'url'
55

6-
spawn('mocha', ['tests', 'tests/csaf_2_1', ...process.argv.slice(2)], {
7-
stdio: 'inherit',
8-
shell: true,
9-
env: {
10-
...process.env,
11-
DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)),
12-
WORDLIST: fileURLToPath(
13-
new URL('../tests/dicts/csaf_words.txt', import.meta.url)
14-
),
15-
},
16-
})
6+
spawn(
7+
'mocha',
8+
['-t', '10000', 'tests', 'tests/csaf_2_1', ...process.argv.slice(2)],
9+
{
10+
stdio: 'inherit',
11+
shell: true,
12+
env: {
13+
...process.env,
14+
DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)),
15+
WORDLIST: fileURLToPath(
16+
new URL('../tests/dicts/csaf_words.txt', import.meta.url)
17+
),
18+
},
19+
}
20+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import assert from 'node:assert'
2+
import { informativeTest_6_3_16 } from '../../csaf_2_1/informativeTests.js'
3+
import { expect } from 'chai'
4+
5+
describe('informativeTest_6_3_16', function () {
6+
it('only runs on relevant documents', async function () {
7+
assert.equal(
8+
(await informativeTest_6_3_16({ document: 'mydoc' })).infos.length,
9+
0
10+
)
11+
})
12+
13+
it('fails if the language is not known', async function () {
14+
const result = await informativeTest_6_3_16({
15+
document: {
16+
lang: 'zz',
17+
},
18+
})
19+
expect(result.infos.length).to.eq(1)
20+
})
21+
})

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const excluded = [
5959
'6.3.13',
6060
'6.3.14',
6161
'6.3.15',
62-
'6.3.16',
6362
'6.3.17',
6463
]
6564

0 commit comments

Comments
 (0)