Skip to content

Commit d3d9dd2

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

File tree

10 files changed

+259
-17
lines changed

10 files changed

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

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)