mirror of
https://github.com/OliveTin/OliveTin
synced 2026-09-23 18:25:36 +00:00
Merge branch 'next' into build/make-generation-reproducible
Resolve Makefile/.PHONY conflicts with docs-check and unittests-race, and address generated-check review feedback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,17 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/codestyle.yml'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
- 'service/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- next
|
||||
paths:
|
||||
- '.github/workflows/codestyle.yml'
|
||||
- 'Makefile'
|
||||
- 'frontend/**'
|
||||
- 'integration-tests/**'
|
||||
- 'proto/**'
|
||||
@@ -35,9 +46,16 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
frontend/package-lock.json
|
||||
integration-tests/package-lock.json
|
||||
|
||||
- name: frontend
|
||||
run: make -wC frontend codestyle
|
||||
|
||||
- name: frontend unit tests
|
||||
run: make -wC frontend unittests
|
||||
|
||||
- name: integration tests codestyle
|
||||
run: make -wC integration-tests codestyle
|
||||
|
||||
@@ -3,12 +3,14 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'service/internal/config/config.go'
|
||||
- 'local-antora-playbook.yml'
|
||||
- 'local-antora-playbook-ci.yml'
|
||||
- '.github/workflows/docs-antora.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'service/internal/config/config.go'
|
||||
- 'local-antora-playbook.yml'
|
||||
- 'local-antora-playbook-ci.yml'
|
||||
- '.github/workflows/docs-antora.yml'
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
- 'lang/**'
|
||||
- 'proto/**'
|
||||
- 'service/gen/**'
|
||||
- 'service/generate.go'
|
||||
- 'service/go.mod'
|
||||
- 'service/go.sum'
|
||||
- 'service/Makefile'
|
||||
@@ -26,6 +27,7 @@ on:
|
||||
- 'lang/**'
|
||||
- 'proto/**'
|
||||
- 'service/gen/**'
|
||||
- 'service/generate.go'
|
||||
- 'service/go.mod'
|
||||
- 'service/go.sum'
|
||||
- 'service/Makefile'
|
||||
@@ -40,6 +42,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Go race detector
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
paths:
|
||||
- '.github/workflows/race.yml'
|
||||
- 'service/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- next
|
||||
paths:
|
||||
- '.github/workflows/race.yml'
|
||||
- 'service/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
race:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
cache-dependency-path: 'service/go.mod'
|
||||
|
||||
- name: Run race detector
|
||||
run: make -wC service unittests-race
|
||||
@@ -33,6 +33,13 @@ repos:
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: docs-check
|
||||
name: docs-check
|
||||
entry: make docs-check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: ^(docs/|service/internal/config/config\.go|Makefile)
|
||||
|
||||
- id: service-codestyle
|
||||
name: service-codestyle
|
||||
entry: make service-codestyle
|
||||
|
||||
@@ -22,6 +22,8 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
|
||||
- From repo root: `cd service && make unittests`
|
||||
- Code style (after editing code in `service/`):
|
||||
- From repo root: `cd service && make codestyle` (runs `go fmt` and `golangci-lint`; install linter via `make go-tools`)
|
||||
- Documentation checks:
|
||||
- From repo root: `make docs-check` (validates documented config key casing and local AsciiDoc anchor links)
|
||||
- Integration tests (Mocha + Selenium):
|
||||
- All tests: `make it` (from repo root; builds webui + service binary, then runs Mocha)
|
||||
- Single test: `cd integration-tests && npx --yes mocha tests/general/general.mjs`
|
||||
|
||||
@@ -63,6 +63,7 @@ The project layout is reasonably straightforward;
|
||||
|
||||
* See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any).
|
||||
* End-user documentation (AsciiDoc for link:https://docs.olivetin.app[docs.olivetin.app]) lives in `docs/` as an Antora component; the published site is built from the separate link:https://github.com/OliveTin/docs.olivetin.app[docs.olivetin.app] repository.
|
||||
* Run `make docs-check` after changing documentation or the service configuration schema.
|
||||
* The API is defined in protobuf+Connect RPC - you will need to `make proto`.
|
||||
* The Go daemon is built from the `cmd` and `internal` directories mostly.
|
||||
* The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver.
|
||||
|
||||
@@ -28,6 +28,10 @@ frontend-codestyle:
|
||||
frontend-unittests:
|
||||
$(MAKE) -wC frontend unittests
|
||||
|
||||
docs-check:
|
||||
python3 docs/modules/ROOT/check_config_keys.py
|
||||
python3 docs/modules/ROOT/check_chevron_links.py
|
||||
|
||||
it:
|
||||
$(MAKE) -wC integration-tests
|
||||
|
||||
@@ -45,6 +49,8 @@ lang-generate:
|
||||
|
||||
generated-check: proto lang-generate
|
||||
git diff --exit-code -- service/gen frontend/resources/scripts/gen lang/combined_output.json
|
||||
@untracked="$$(git ls-files --others --exclude-standard -- service/gen frontend/resources/scripts/gen lang/combined_output.json)"; \
|
||||
test -z "$$untracked" || { printf 'Untracked generated files:\n%s\n' "$$untracked"; exit 1; }
|
||||
|
||||
dist:
|
||||
echo "dist noop"
|
||||
@@ -90,4 +96,4 @@ config-tool:
|
||||
devcheck:
|
||||
python3 scripts/devcheck.py $(ARGS)
|
||||
|
||||
.PHONY: proto proto-tools lang-generate generated-check default service windows-resources windows-msi frontend-unittests it devcheck
|
||||
.PHONY: proto proto-tools lang-generate generated-check default service windows-resources windows-msi frontend-unittests docs-check it devcheck
|
||||
|
||||
+7
-6
@@ -2,10 +2,11 @@ define delete-files
|
||||
python3 -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
codestyle:
|
||||
npm install
|
||||
npx eslint --fix main.js js/* resources/vue
|
||||
npx stylelint style.css
|
||||
codestyle: deps
|
||||
npm run lint
|
||||
|
||||
codestyle-fix: deps
|
||||
npm run lint:fix
|
||||
|
||||
unittests: deps
|
||||
npm test
|
||||
@@ -14,11 +15,11 @@ clean:
|
||||
$(call delete-files,dist)
|
||||
|
||||
deps:
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
build:
|
||||
npx vite build
|
||||
|
||||
dist: deps clean build
|
||||
|
||||
.PHONY: codestyle unittests
|
||||
.PHONY: codestyle codestyle-fix unittests deps build dist clean
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"stylelint-config-standard": "^40.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint main.js js/* resources/vue vite.config.mjs && stylelint \"style.css\" \"themes/**/*.css\"",
|
||||
"lint:fix": "eslint --fix main.js js/* resources/vue vite.config.mjs && stylelint --fix \"style.css\" \"themes/**/*.css\"",
|
||||
"test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs resources/vue/stores/*.test.mjs"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
+12
-12
@@ -4,16 +4,16 @@ import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
dedupe: ['vue', 'vue-router'],
|
||||
dedupe: ['vue', 'vue-router']
|
||||
},
|
||||
plugins: [
|
||||
Components({
|
||||
dirs: ['resources/vue/'],
|
||||
extensions: ['vue'],
|
||||
deep: true,
|
||||
dts: false,
|
||||
dts: false
|
||||
}),
|
||||
vue(),
|
||||
vue()
|
||||
],
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
@@ -22,25 +22,25 @@ export default defineConfig({
|
||||
return
|
||||
}
|
||||
defaultHandler(level, log)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
secure: false
|
||||
},
|
||||
'/theme.css': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
secure: false
|
||||
},
|
||||
"/custom-webui": {
|
||||
target: "http://localhost:1337",
|
||||
changeOrigin: true,
|
||||
'/custom-webui': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
env:
|
||||
browser: true
|
||||
es2021: true
|
||||
extends: 'eslint:recommended'
|
||||
parserOptions:
|
||||
ecmaVersion: 12
|
||||
sourceType: module
|
||||
rules: {}
|
||||
@@ -1,7 +1,13 @@
|
||||
default: test-install prep test-run
|
||||
|
||||
test-install:
|
||||
npm install --no-fund
|
||||
npm ci --no-fund
|
||||
|
||||
codestyle: test-install
|
||||
npm run lint
|
||||
|
||||
codestyle-fix: test-install
|
||||
npm run lint:fix
|
||||
|
||||
prep:
|
||||
ifneq ($(SKIP_WEBUI),1)
|
||||
@@ -11,7 +17,7 @@ endif
|
||||
|
||||
test-run:
|
||||
# GitHub Actions fails badly on the default timeout of 2000ms
|
||||
npx mocha tests --recursive -t 10000
|
||||
npm test
|
||||
|
||||
find-flakey-tests:
|
||||
echo "Running test-run infinately"
|
||||
@@ -30,4 +36,4 @@ getsnapshot:
|
||||
rm -rf /opt/OliveTin-snapshot/*
|
||||
gh run download -D /opt/OliveTin-snapshot/
|
||||
|
||||
.PHONY: default find-flakey-tests find-flakey-tests-inf prep
|
||||
.PHONY: default test-install codestyle codestyle-fix test-run find-flakey-tests find-flakey-tests-inf prep
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'tests/customJs/custom-webui/**'
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{js,mjs}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
after: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
before: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
describe: 'readonly',
|
||||
it: 'readonly',
|
||||
runner: 'readonly',
|
||||
webdriver: 'readonly'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -114,7 +114,7 @@ export function takeScreenshot (webdriver, title) {
|
||||
fs.mkdirSync('screenshots', { recursive: true });
|
||||
|
||||
title = title.replaceAll('config: ', '')
|
||||
title = title.replaceAll(/[\(\)\|\*\<\>\:]/g, "_")
|
||||
title = title.replaceAll(/[()|*<>:]/g, '_')
|
||||
title = title + '.failed-test'
|
||||
|
||||
fs.writeFileSync('screenshots/' + title + '.png', img, 'base64')
|
||||
|
||||
Generated
+36
@@ -12,8 +12,10 @@
|
||||
"wait-on": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.10.0",
|
||||
"globals": "^17.3.0",
|
||||
"mocha": "^12.0.0",
|
||||
"selenium-webdriver": "^4.49.0"
|
||||
}
|
||||
@@ -141,6 +143,27 @@
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
|
||||
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
|
||||
@@ -966,6 +989,19 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz",
|
||||
"integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"lint": "eslint tests lib scripts runner.mjs mochaSetup.mjs",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "mocha tests --recursive -t 10000"
|
||||
},
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"chai": "^6.2.2",
|
||||
"eslint": "^10.10.0",
|
||||
"globals": "^17.3.0",
|
||||
"mocha": "^12.0.0",
|
||||
"selenium-webdriver": "^4.49.0"
|
||||
},
|
||||
|
||||
@@ -104,7 +104,7 @@ function runMochaOnce () {
|
||||
try {
|
||||
report = JSON.parse(readFileSync(reportPath, 'utf8'))
|
||||
} catch {
|
||||
report = null
|
||||
// Keep the default null report when Mocha did not produce valid JSON.
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function waitForActionSuccessFlash (actionTitle) {
|
||||
const button = await getActionButton(webdriver, actionTitle)
|
||||
const classAttr = await button.getAttribute('class')
|
||||
return classAttr && classAttr.includes('action-success')
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -45,7 +45,7 @@ async function waitForTerminalOutput (expectedSubstring) {
|
||||
|
||||
const output = await getTerminalBuffer()
|
||||
return output && output.includes(expectedSubstring)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async function waitForTerminalOutput(expectedValue) {
|
||||
}
|
||||
|
||||
return output.trim().includes(`Checkbox value: ${expectedValue}`)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -44,7 +44,7 @@ async function pollTerminal(matcher, timeoutMs = DEFAULT_UI_WAIT_MS) {
|
||||
}
|
||||
|
||||
return matcher(output.trim())
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -59,7 +59,7 @@ async function waitForTerminalOutput (expectedSubstring) {
|
||||
|
||||
const output = await getTerminalBuffer()
|
||||
return output && output.includes(expectedSubstring)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect, assert } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import { By } from 'selenium-webdriver'
|
||||
//import * as waitOn from 'wait-on'
|
||||
import {
|
||||
getRootAndWait,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
//import * as waitOn from 'wait-on'
|
||||
import {
|
||||
getRootAndWait,
|
||||
|
||||
@@ -2,9 +2,7 @@ import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
import {
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ async function waitForTerminalOutput (expectedSubstring) {
|
||||
|
||||
const output = await getTerminalBuffer()
|
||||
return output && output.includes(expectedSubstring)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
@@ -47,7 +46,7 @@ describe('config: localAuth', function () {
|
||||
// Check if any login-related elements are present
|
||||
const bodyText = await webdriver.findElement(By.tagName('body')).getText()
|
||||
console.log('Login page content:', bodyText.substring(0, 300))
|
||||
|
||||
|
||||
// For now, just verify we can navigate to the login page
|
||||
// The page content rendering is a separate frontend issue
|
||||
console.log('Login page navigation successful')
|
||||
@@ -67,11 +66,11 @@ describe('config: localAuth', function () {
|
||||
|
||||
if (usernameFields.length > 0 && passwordFields.length > 0 && loginButtons.length > 0) {
|
||||
console.log('Login form found, attempting login')
|
||||
|
||||
|
||||
// Fill in credentials
|
||||
await usernameFields[0].clear()
|
||||
await usernameFields[0].sendKeys('testuser')
|
||||
|
||||
|
||||
await passwordFields[0].clear()
|
||||
await passwordFields[0].sendKeys('testpass123')
|
||||
|
||||
@@ -100,4 +99,4 @@ describe('config: localAuth', function () {
|
||||
console.log('Login form not found - skipping login test')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
@@ -210,7 +209,7 @@ describe('config: logPersistence', function () {
|
||||
const text = await body.getText()
|
||||
// The log should contain the output from the echo command
|
||||
return text.includes('Hello from persisted log test') || text.includes(firstExecutionTrackingId)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition, Key } from 'selenium-webdriver'
|
||||
import { By, Condition, Key } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
@@ -56,14 +55,14 @@ describe('config: githubOAuth', function () {
|
||||
// Button may show "Login with GitHub" or "Login with undefined" depending on provider.name vs provider.title
|
||||
// We'll check for the presence of the button and verify it's in the OAuth section
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||
|
||||
|
||||
// The first button should be GitHub since it's the only provider in the config
|
||||
const githubButton = githubButtons[0]
|
||||
const buttonText = await githubButton.getText()
|
||||
|
||||
|
||||
// Button should contain "Login with" and the provider should be configured as GitHub
|
||||
expect(buttonText).to.include('Login with', 'Button should have "Login with" prefix')
|
||||
|
||||
|
||||
console.log('GitHub OAuth button found with text:', buttonText)
|
||||
})
|
||||
|
||||
@@ -78,16 +77,14 @@ describe('config: githubOAuth', function () {
|
||||
// Since the test config only has one provider (GitHub), we can use the first button
|
||||
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||
|
||||
|
||||
const githubButton = githubButtons[0]
|
||||
const buttonText = await githubButton.getText()
|
||||
console.log('Button text:', buttonText)
|
||||
|
||||
|
||||
// Verify it's the GitHub button (should contain "github" in the text)
|
||||
expect(buttonText.toLowerCase()).to.include('github', 'Button should be GitHub OAuth button')
|
||||
|
||||
// Check for provider icon (if present)
|
||||
const providerIcons = await githubButton.findElements(By.css('.provider-icon'))
|
||||
const providerNames = await githubButton.findElements(By.css('.provider-name'))
|
||||
// Provider name may show "GitHub" (from title) or be undefined (if using name field)
|
||||
// Just verify the structure is present
|
||||
@@ -111,11 +108,8 @@ describe('config: githubOAuth', function () {
|
||||
// Find GitHub OAuth button (should be the first/only one in our test config)
|
||||
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'OAuth button should be present')
|
||||
|
||||
const githubButton = githubButtons[0]
|
||||
|
||||
// Get the current URL before clicking
|
||||
const initialUrl = await webdriver.getCurrentUrl()
|
||||
const githubButton = githubButtons[0]
|
||||
|
||||
// Click the button
|
||||
await githubButton.click()
|
||||
@@ -124,7 +118,7 @@ describe('config: githubOAuth', function () {
|
||||
// Since we can't actually complete OAuth flow, we check that the button
|
||||
// click handler is set up correctly by verifying the button exists and is clickable
|
||||
// In a real scenario, this would redirect to GitHub's OAuth page
|
||||
|
||||
|
||||
// Give a small delay to allow any navigation to start
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
@@ -133,4 +127,3 @@ describe('config: githubOAuth', function () {
|
||||
console.log('GitHub OAuth button click verified (redirect would happen in production)')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
getActionButtons,
|
||||
getNavigationLinks,
|
||||
openSidebar,
|
||||
closeSidebar,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
@@ -43,7 +42,7 @@ describe('config: onlyDashboards', function () {
|
||||
const firstDashboardLink = await webdriver.findElement(By.css('li[title="My Dashboard"]'), 'The first dashboard link should be present')
|
||||
assert.isNotNull(firstDashboardLink, 'First dashboard link should not be null')
|
||||
assert.isTrue(await firstDashboardLink.isDisplayed(), 'First dashboard link should be displayed')
|
||||
|
||||
|
||||
const actionButtonsOnDashboard = await getActionButtons()
|
||||
assert.isArray(actionButtonsOnDashboard, 'Action buttons on dashboard should be an array')
|
||||
assert.lengthOf(actionButtonsOnDashboard, 3, 'Action buttons on dashboard should have 3 buttons')
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as process from 'node:process'
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshot,
|
||||
takeScreenshotOnFailure,
|
||||
findExecutionDialog,
|
||||
requireExecutionDialogStatus,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
@@ -116,13 +115,13 @@ describe('config: stdout-most-recent-execution', function () {
|
||||
// Output should change from initial state and contain actual output
|
||||
// (not "Waiting...", "No execution found", or the same as initialText)
|
||||
const hasChanged = newText !== initialText
|
||||
const hasValidOutput = newText &&
|
||||
!newText.includes('Waiting...') &&
|
||||
!newText.includes('No execution found') &&
|
||||
const hasValidOutput = newText &&
|
||||
!newText.includes('Waiting...') &&
|
||||
!newText.includes('No execution found') &&
|
||||
!newText.includes('Error:') &&
|
||||
newText.trim().length > 0
|
||||
return hasChanged && hasValidOutput
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
|
||||
+4
-1
@@ -44,6 +44,9 @@ unittests:
|
||||
unittests-fast:
|
||||
go test ./... -count=1
|
||||
|
||||
unittests-race:
|
||||
go test -race ./... -count=1
|
||||
|
||||
find-flakey-tests:
|
||||
echo "Running unittests-fast infinitely"
|
||||
sh -c "while $(MAKE) unittests-fast; do :; done"
|
||||
@@ -60,4 +63,4 @@ proto-tools:
|
||||
|
||||
go-tools-all: go-tools proto-tools
|
||||
|
||||
.PHONY: codestyle go-tools proto-tools go-tools-all unittests unittests-fast find-flakey-tests find-flakey-tests-inf
|
||||
.PHONY: codestyle go-tools proto-tools go-tools-all unittests unittests-fast unittests-race find-flakey-tests find-flakey-tests-inf
|
||||
|
||||
@@ -153,6 +153,30 @@ type InternalLogEntry struct {
|
||||
TimedOut bool
|
||||
}
|
||||
|
||||
func cloneActionBinding(binding *ActionBinding) *ActionBinding {
|
||||
if binding == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *binding
|
||||
cloned.OnDashboards = slices.Clone(binding.OnDashboards)
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneInternalLogEntry(entry *InternalLogEntry) *InternalLogEntry {
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *entry
|
||||
cloned.Arguments = maps.Clone(entry.Arguments)
|
||||
cloned.Tags = slices.Clone(entry.Tags)
|
||||
cloned.Binding = cloneActionBinding(entry.Binding)
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
// .Binding can be nil, so we need to handle that.
|
||||
func (e *InternalLogEntry) GetBindingId() string {
|
||||
if e.Binding == nil {
|
||||
@@ -273,7 +297,7 @@ func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*Int
|
||||
|
||||
if totalLogCount > 0 {
|
||||
for i := startIndex; i >= endIndex; i-- {
|
||||
trackingIds = append(trackingIds, e.logs[e.logsTrackingIdsByDate[i]])
|
||||
trackingIds = append(trackingIds, cloneInternalLogEntry(e.logs[e.logsTrackingIdsByDate[i]]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +327,7 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
|
||||
entry := e.logs[trackingId]
|
||||
|
||||
if shouldIncludeLogEntry(cfg, user, entry, filterDate, hasDateFilter) {
|
||||
filtered = append(filtered, entry)
|
||||
filtered = append(filtered, cloneInternalLogEntry(entry))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,26 +423,28 @@ func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.Aut
|
||||
|
||||
func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
|
||||
e.logmutex.RLock()
|
||||
defer e.logmutex.RUnlock()
|
||||
|
||||
entry, found := e.logs[trackingID]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
return entry, found
|
||||
return cloneInternalLogEntry(entry), found
|
||||
}
|
||||
|
||||
func (e *Executor) GetLogsByBindingId(bindingId string) []*InternalLogEntry {
|
||||
e.logmutex.RLock()
|
||||
defer e.logmutex.RUnlock()
|
||||
|
||||
logs, found := e.LogsByBindingId[bindingId]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
if !found {
|
||||
return make([]*InternalLogEntry, 0)
|
||||
}
|
||||
|
||||
return logs
|
||||
cloned := make([]*InternalLogEntry, 0, len(logs))
|
||||
for _, entry := range logs {
|
||||
cloned = append(cloned, cloneInternalLogEntry(entry))
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
// shouldCountExecution checks if a log entry should be counted for rate limiting.
|
||||
|
||||
@@ -38,6 +38,39 @@ func testingExecutor() (*Executor, *config.Config) {
|
||||
return e, cfg
|
||||
}
|
||||
|
||||
func TestGetLogReturnsDefensiveCopy(t *testing.T) {
|
||||
e := DefaultExecutor(config.DefaultConfig())
|
||||
e.logs["tracking-id"] = &InternalLogEntry{
|
||||
Arguments: map[string]string{"message": "original"},
|
||||
Output: "original",
|
||||
Tags: []string{"original"},
|
||||
Binding: &ActionBinding{
|
||||
ID: "original-binding",
|
||||
OnDashboards: []DashboardNavigationTarget{
|
||||
{Title: "original"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
entry, found := e.GetLog("tracking-id")
|
||||
require.True(t, found)
|
||||
|
||||
entry.Arguments["message"] = "changed"
|
||||
entry.Output = "changed"
|
||||
entry.Tags[0] = "changed"
|
||||
entry.Binding.ID = "changed-binding"
|
||||
entry.Binding.OnDashboards[0].Title = "changed"
|
||||
|
||||
stored, found := e.GetLog("tracking-id")
|
||||
require.True(t, found)
|
||||
assert.Equal(t, "original", stored.Arguments["message"])
|
||||
assert.Equal(t, "original", stored.Output)
|
||||
assert.Equal(t, []string{"original"}, stored.Tags)
|
||||
require.NotNil(t, stored.Binding)
|
||||
assert.Equal(t, "original-binding", stored.Binding.ID)
|
||||
assert.Equal(t, []DashboardNavigationTarget{{Title: "original"}}, stored.Binding.OnDashboards)
|
||||
}
|
||||
|
||||
func TestCreateExecutorAndExec(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
|
||||
@@ -273,13 +273,17 @@ func processDebounce(ctx *watchContext) {
|
||||
if logEntry.callbackComplete || logEntry.callbackWrapper == nil {
|
||||
log.Debugf("fsnotify event callback queued within debounce delay: %v", ctx.filename)
|
||||
|
||||
callback := ctx.callback
|
||||
eventName := ctx.event.Name
|
||||
logEntry.callbackComplete = false
|
||||
logEntry.callbackWrapper = time.AfterFunc(debounceDelay, func() {
|
||||
log.Debugf("fsnotify event callback being fired: %v", ctx.filename)
|
||||
log.Debugf("fsnotify event callback being fired: %v", eventName)
|
||||
|
||||
ctx.callback(ctx.event.Name)
|
||||
callback(eventName)
|
||||
|
||||
debounceWriteLogMutex.Lock()
|
||||
logEntry.callbackComplete = true
|
||||
debounceWriteLogMutex.Unlock()
|
||||
})
|
||||
} else {
|
||||
log.Debugf("fsnotify event suppressed because it's within the debounce delay: %v", ctx.filename)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package filehelper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcessDebounceCapturesEventName(t *testing.T) {
|
||||
debounceWriteLogMutex.Lock()
|
||||
debounceWriteLog = make(map[string]*FsNotifyLogEntry)
|
||||
debounceWriteLogMutex.Unlock()
|
||||
|
||||
callbackNames := make(chan string, 1)
|
||||
firstEvent := fsnotify.Event{Name: "first"}
|
||||
ctx := &watchContext{
|
||||
callback: func(filename string) {
|
||||
callbackNames <- filename
|
||||
},
|
||||
event: &firstEvent,
|
||||
filename: t.Name(),
|
||||
}
|
||||
|
||||
processDebounce(ctx)
|
||||
|
||||
secondEvent := fsnotify.Event{Name: "second"}
|
||||
ctx.event = &secondEvent
|
||||
|
||||
select {
|
||||
case callbackName := <-callbackNames:
|
||||
require.Equal(t, firstEvent.Name, callbackName)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("debounced callback did not run")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user