Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ jobs:
with:
node-version: 24.x
- run: sudo apt-get install hunspell
- name: Start language tool
run: docker compose -f dev/languagetool/compose.yml up -d
- name: Wait for app start
uses: ifaxity/wait-on-action@v1
with:
delay: 1
timeout: 120000
resource: http://localhost:8010/v2/languages
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm test
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Start language tool
run: docker compose -f dev/languagetool/compose.yml up -d
- name: Wait for app start
uses: ifaxity/wait-on-action@v1
with:
delay: 1
timeout: 120000
resource: http://localhost:8010/v2/languages
- run: npm ci
- run: npm run test-report
env:
Expand Down
7 changes: 7 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

## Table of Contents

- [Language Tool](#language-tool)
- [Code Style](#code-style)
- [Formatting with prettier](#formatting-with-prettier)
- [Quoting Strings](#quoting-strings)

## Language Tool

The informative test 6.3.16 needs a running languagetool server. To set one for development you can use `dev/languagetool/compose.yml`:

docker compose -f dev/languagetool/compose.yml up -d

## Code Style

### Formatting with prettier
Expand Down
16 changes: 15 additions & 1 deletion README.md
Comment thread
bendo-eXX marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ You can also include this library as a subtree in your repository.

- For test 6.3.8 an installation of hunspell as well as all languages that
you want to spell check is required.
- For test 6.3.16 a running LanguageTool server is required.
See [Running LanguageTool for informative test 6.3.16](#running-languagetool-for-informative-test-6316) for more
information.

### Managing Hunspell languages

Expand All @@ -103,6 +106,17 @@ repository of your distribution. If you have a custom dictionary
copy them in the directory provided by the command above. Hunspell should
automatically recognize them.

### Running LanguageTool for informative test 6.3.16

Informative test 6.3.16 requires a running LanguageTool server.
The default URL in this library is `http://localhost:8010`.

You can start LanguageTool with Docker Compose from the repository root:

```sh
docker compose -f dev/languagetool/compose.yml up -d
```

[(back to top)](#bsi-csaf-validator-lib)

## How to use
Expand Down Expand Up @@ -381,7 +395,6 @@ The following tests are not yet implemented and therefore missing:
- Informative Test 6.3.13
- Informative Test 6.3.14
- Informative Test 6.3.15
- Informative Test 6.3.16
- Informative Test 6.3.17
- Informative Test 6.3.19.1
- Informative Test 6.3.19.2
Expand Down Expand Up @@ -546,6 +559,7 @@ export const informativeTest_6_3_9: DocumentTest
export const informativeTest_6_3_10: DocumentTest
export const informativeTest_6_3_11: DocumentTest
export const informativeTest_6_3_12: DocumentTest
export const informativeTest_6_3_16: DocumentTest
export const informativeTest_6_3_18: DocumentTest
export const informativeTest_6_3_20: DocumentTest
export const informativeTest_6_3_21_1: DocumentTest
Expand Down
13 changes: 13 additions & 0 deletions context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @typedef {object} Context
* @property {string} languageToolUrl The url to the language tool
*/

/**
* This is the context that is used to execute the tests. Modify it when
* initializing the library to change settings. Port 8010 is the default exposed
* by the collabora/languagetool image in `dev/languagetool/compose.yml`.
*
* @type {Context}
*/
export const context = { languageToolUrl: 'http://localhost:8010' }
Comment thread
bendo-eXX marked this conversation as resolved.
1 change: 1 addition & 0 deletions csaf_2_1/informativeTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.
export { informativeTest_6_3_5 } from './informativeTests/informativeTest_6_3_5.js'
export { informativeTest_6_3_6 } from './informativeTests/informativeTest_6_3_6.js'
export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js'
export { informativeTest_6_3_16 } from './informativeTests/informativeTest_6_3_16.js'
export { informativeTest_6_3_18 } from './informativeTests/informativeTest_6_3_18.js'
export { informativeTest_6_3_20 } from './informativeTests/informativeTest_6_3_20.js'
export { informativeTest_6_3_21_1 } from './informativeTests/informativeTest_6_3_21_1.js'
Expand Down
209 changes: 209 additions & 0 deletions csaf_2_1/informativeTests/informativeTest_6_3_16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
This test depends on the languagetool server to be available. See
https://languagetool.org/de. A compose file is available at
`dev/languagetool/compose.yml` to start an instance.
*/

import { Ajv } from 'ajv/dist/jtd.js'
import bcp47 from 'bcp47'
import { context } from '../../context.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
document: {
additionalProperties: true,
optionalProperties: {
lang: { type: 'string' },
},
},
},
})

const validateInput = ajv.compile(inputSchema)

/**
* If the document language is given it MUST be tested that a grammar check for
* the given language does not find any mistakes. The test SHALL be skipped if
* the document language is not set. It SHALL fail if the given language is not
* supported.
*
* @param {unknown} doc
* @returns
*/
export async function informativeTest_6_3_16(doc) {
const ctx = {
infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]),
}

if (!validateInput(doc)) {
return ctx
}

const lang = doc.document?.lang
if (!lang) {
return ctx
}

const fallbackLang = bcp47.parse(lang)?.langtag.language.language

/*
Check if the language is supported by the languagetool server.
*/
{
/**
* @typedef {object} Language
* @property {string} code
* @property {string} [longCode]
*/

/** @typedef {Language[]} Response */

const res = await fetch(new URL('/v2/languages', context.languageToolUrl), {
headers: {
accept: 'application/json',
},
})
if (!res.ok) {
throw new Error(
`request to languagetool at ${context.languageToolUrl} failed`
)
}

const json = /** @type {Response} */ (await res.json())

if (!json.some((l) => l.longCode === lang || l.code === fallbackLang)) {
ctx.infos.push({
instancePath: '/document/lang',
message: `language '${lang}' is not supported by languagetool`,
})
}
}

for (const path of [
'/document/acknowledgments[]/summary',
'/document/aggregate_severity/text',
'/document/distribution/text',
'/document/notes[]/audience',
'/document/notes[]/text',
'/document/notes[]/title',
'/document/publisher/issuing_authority',
'/document/references[]/summary',
'/document/title',
'/document/tracking/revision_history[]/summary',
'/product_tree/product_groups[]/summary',
'/vulnerabilities[]/acknowledgments[]/summary',
'/vulnerabilities[]/involvements[]/summary',
'/vulnerabilities[]/notes[]/audience',
'/vulnerabilities[]/notes[]/text',
'/vulnerabilities[]/notes[]/title',
'/vulnerabilities[]/references[]/summary',
'/vulnerabilities[]/remediations[]/details',
'/vulnerabilities[]/remediations[]/entitlements[]',
'/vulnerabilities[]/remediations[]/restart_required/details',
'/vulnerabilities[]/threats[]/details',
'/vulnerabilities[]/title',
]) {
await checkPath(
[],
path.split('/').slice(1),
doc,
async (instancePath, text) => {
if (typeof text !== 'string') return
const result = await checkString(text, lang)
if (result.length) {
ctx.infos.push({
instancePath,
message: result.map((r) => r.message).join(' '),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should open an issue as a reminder to improve the output and expose all information language tool provides us... (This will be helpful for viewers to display and provide correction suggestions...

})
}
}
)
}

return ctx
}

/**
* Checks the value behind `path` using the given `onCheck` function. This is a
* recursive helper function to loop through the list of paths in the spec.
*
* @param {string[]} reminder
* @param {string[]} path
* @param {unknown} value
* @param {(instancePath: string, value: string) => Promise<void>} onCheck
*/
async function checkPath(reminder, path, value, onCheck) {
if (value == null) return
const currentSegment = path.at(0)

if (!currentSegment) {
// We've reached the end. Now the `onCheck` function can be called to check
// the actual value.
if (typeof value === 'string') {
await onCheck('/' + reminder.join('/'), value)
}
} else if (currentSegment.endsWith('[]')) {
// The value is supposed to be an array for which every element needs to be
// checked ...
const arrayName = currentSegment.split('[')[0]
const array = Reflect.get(value, arrayName)

if (Array.isArray(array)) {
// ... But only if it's really an array.
for (const [elementIndex, element] of array.entries() ?? []) {
await checkPath(
[...reminder, arrayName, String(elementIndex)],
path.slice(1),
element,
onCheck
)
}
}
} else {
// Otherwise it's something object-ish which we traverse recursively.
await checkPath(
[...reminder, currentSegment],
path.slice(1),
Reflect.get(value, currentSegment),
onCheck
)
}
}

/**
* Check the given string using the languagetool server.
*
* @param {string} str
* @param {string} lng
* @returns
*/
async function checkString(str, lng) {
/**
* @typedef {object} Match
* @property {string} message
*/

/**
* @typedef {object} Response
* @property {Match[]} matches
*/

const res = await fetch(new URL('/v2/check', context.languageToolUrl), {
method: 'POST',
body: new URLSearchParams([
['language', lng],
['text', str],
]),
})
if (!res.ok) {
throw new Error(
`request to languagetool at ${context.languageToolUrl} failed`
)
}

const json = /** @type {Response} */ (await res.json())
return json.matches
}
6 changes: 6 additions & 0 deletions dev/languagetool/compose.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to name that or a better location? In my perception, people think, when they find a docker file in the root of the repo that this is for the repo itself, not a dev dependency...

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Optional dev dependency: only needed to run informative test 6.3.16
services:
languagetool:
image: collabora/languagetool
ports:
- 8010:8010
30 changes: 30 additions & 0 deletions tests/csaf_2_1/informativeTest_6_3_16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { informativeTest_6_3_16 } from '../../csaf_2_1/informativeTests.js'
import isBrowserRuntime from '../shared/isBrowserRuntime.js'

describe('informativeTest_6_3_16', function () {
it('only runs on relevant documents', async function () {
expect(
(await informativeTest_6_3_16({ document: 'mydoc' })).infos.length
).to.eq(0)
})

it('is skipped when no document language is set', async function () {
const result = await informativeTest_6_3_16({ document: {} })
expect(result.infos.length).to.eq(0)
})

// This test requires a running languagetool server (http://localhost:8010,
// see dev/languagetool/compose.yml), which is not started for the browser
// test project - skip it here, analogous to tests/csaf_2_1/oasis.js.
it.skipIf(isBrowserRuntime)(
'fails if the language is not known',
async function () {
const result = await informativeTest_6_3_16({
document: {
lang: 'zz',
},
})
expect(result.infos.length).to.eq(1)
}
)
})
Loading
Loading