1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +02:00

task: migrate tests to vitest

Vitest Pros:
* Automated failing test comments on github PRs
* A nice local UI with incremental testing when changing files (`yarn
test:ui`)
* Also nicely supported in all major IDEs, click to run test works (so
we won't miss what we had with jest).
* Works well with ESM

Vitest Cons:
* The ESBuild transformer vitest uses takes a little longer to transform
than our current SWC/jest setup, however, it is possible to setup SWC as
the transformer for vitest as well (though it only does one transform,
so we're paying ~7-10 seconds instead of ~ 2-3 seconds in transform
phase).
* Exposes how slow our tests are (tongue in cheek here)
This commit is contained in:
Christopher Kolstad 2025-05-16 11:19:10 +02:00 committed by GitHub
parent 4d1b44818f
commit b681702b77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
119 changed files with 2324 additions and 96108 deletions

View File

@ -16,7 +16,10 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: build name: build
permissions:
contents: read
# Needed for the github-reporter from vite to make comments on PRs
pull-requests: write
services: services:
# Label used to access the service container # Label used to access the service container
postgres: postgres:
@ -47,14 +50,8 @@ jobs:
YARN_ENABLE_SCRIPTS: false YARN_ENABLE_SCRIPTS: false
- run: yarn lint - run: yarn lint
- run: yarn build:backend - run: yarn build:backend
- run: yarn run test:report # This adds test results as github check to the workflow - run: yarn run test
env: env:
CI: true CI: true
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
- name: Upload test report to build # Done this way since external PRs would not be able to write the check. See https://github.com/marketplace/actions/test-reporter#recommended-setup-for-public-repositories
uses: actions/upload-artifact@v4
if: (success() || failure()) && github.ref == 'refs/heads/main'
with:
name: test-results
path: ./reports/jest-junit.xml

View File

@ -12,6 +12,9 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull_requests: write
name: build # temporary solution to trick branch protection rules name: build # temporary solution to trick branch protection rules
services: services:
@ -32,31 +35,25 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js 20.x - name: Use Node.js 22.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22.x node-version: 22.x
cache: 'yarn' cache: 'yarn'
- name: Enable corepack - name: Enable corepack
run: corepack enable run: corepack enable
- run: yarn build:backend - name: Install deps
- name: Tests on 20.x run: yarn install
- name: Build backend
run: yarn backend
- name: Tests on 22.x
id: coverage id: coverage
uses: ArtiomTr/jest-coverage-report-action@v2 run: yarn test:coverage
with:
annotations: none
package-manager: yarn
test-script: yarn run test:coverage:jest
base-coverage-file: ./coverage/report.json
output: report-markdown
env: env:
CI: true CI: true
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
NODE_ENV: test NODE_ENV: test
PORT: 4243 PORT: 4243
# - name: Report coverage on ${{ matrix.node-version }} - name: Report coverage
# uses: marocchino/sticky-pull-request-comment@v2 uses: davelosert/vitest-coverage-report-action@v2
# with:
# # pass output from the previous step by id.
# message: ${{ steps.coverage.outputs.report }}

1
.gitignore vendored
View File

@ -107,3 +107,4 @@ website/.yarn/*
!website/.yarn/sdks !website/.yarn/sdks
!website/.yarn/versions !website/.yarn/versions
coverage/.tmp

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -53,15 +53,15 @@
"local:package": "del-cli --force build && mkdir build && cp -r dist docs CHANGELOG.md LICENSE README.md package.json build", "local:package": "del-cli --force build && mkdir build && cp -r dist docs CHANGELOG.md LICENSE README.md package.json build",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"prepare": "husky && yarn --cwd ./frontend install && if [ ! -d ./dist ]; then yarn build; fi", "prepare": "husky && yarn --cwd ./frontend install && if [ ! -d ./dist ]; then yarn build; fi",
"test": "NODE_ENV=test PORT=4243 node --experimental-vm-modules node_modules/.bin/jest", "test": "NODE_ENV=test PORT=4243 vitest run",
"test:unit": "NODE_ENV=test PORT=4243 node --experimental-vm-modules node_modules/.bin/jest --testPathIgnorePatterns=src/test/e2e", "test:unit": "NODE_ENV=test PORT=4243 vitest --exclude src/test/e2e",
"test:docker": "./scripts/docker-postgres.sh", "test:docker": "./scripts/docker-postgres.sh",
"test:report": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test PORT=4243 node node_modules/.bin/jest --reporters=\"default\" --reporters=\"jest-junit\"", "test:report": "NODE_ENV=test PORT=4243 vitest --reporter=junit",
"test:docker:cleanup": "docker rm -f unleash-postgres", "test:docker:cleanup": "docker rm -f unleash-postgres",
"test:watch": "yarn test --watch", "test:watch": "vitest",
"test:coverage": "NODE_ENV=test PORT=4243 node --experimental-vm-modules node_modules/.bin/jest --coverage --testLocationInResults --outputFile=\"coverage/report.json\" --forceExit", "test:coverage": "NODE_ENV=test PORT=4243 vitest run --coverage --outputFile=\"coverage/report.json\"",
"test:coverage:jest": "NODE_ENV=test PORT=4243 node --experimental-vm-modules node_modules/.bin/jest --silent --ci --json --coverage --testLocationInResults --outputFile=\"report.json\" --forceExit", "test:updateSnapshot": "NODE_ENV=test PORT=4243 vitest run -u",
"test:updateSnapshot": "NODE_ENV=test PORT=4243 node --experimental-vm-modules node_modules/.bin/jest --updateSnapshot", "test:ui": "vitest --ui",
"seed:setup": "ts-node src/test/e2e/seed/segment.seed.ts", "seed:setup": "ts-node src/test/e2e/seed/segment.seed.ts",
"seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev", "seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev",
"clean": "del-cli --force dist", "clean": "del-cli --force dist",
@ -69,66 +69,6 @@
"prepack": "./scripts/prepack.sh", "prepack": "./scripts/prepack.sh",
"schema:update": "node ./.husky/update-openapi-spec-list.js" "schema:update": "node ./.husky/update-openapi-spec-list.js"
}, },
"jest-junit": {
"suiteName": "Unleash Unit Tests",
"outputDirectory": "./reports",
"outputName": "jest-junit.xml",
"uniqueOutputName": "false",
"classNameTemplate": "{classname}-{title}",
"titleTemplate": "{classname}-{title}",
"ancestorSeparator": " ",
"usePathForSuiteName": "true"
},
"jest": {
"automock": false,
"maxWorkers": 4,
"testTimeout": 20000,
"globalSetup": "./src/jest-setup.ts",
"transform": {
"^.+\\.tsx?$": [
"@swc/jest",
{
"jsc": {
"target": "ESNext",
"parser": {
"syntax": "typescript"
}
},
"isModule": true
}
]
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/",
"/frontend/",
"/website/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/dist/",
"/src/migrations",
"/src/test"
],
"extensionsToTreatAsEsm": [
".ts",
".tsx"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
},
"transformIgnorePatterns": [
"node_modules"
]
},
"dependencies": { "dependencies": {
"@slack/web-api": "^7.9.1", "@slack/web-api": "^7.9.1",
"@wesleytodd/openapi": "^1.1.0", "@wesleytodd/openapi": "^1.1.0",
@ -199,15 +139,14 @@
"@babel/core": "7.26.10", "@babel/core": "7.26.10",
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@cyclonedx/yarn-plugin-cyclonedx": "^2.0.0", "@cyclonedx/yarn-plugin-cyclonedx": "^2.0.0",
"@fast-check/vitest": "^0.2.1",
"@swc/core": "1.11.24", "@swc/core": "1.11.24",
"@swc/jest": "0.2.38",
"@types/bcryptjs": "2.4.6", "@types/bcryptjs": "2.4.6",
"@types/cors": "2.8.17", "@types/cors": "2.8.17",
"@types/express": "4.17.21", "@types/express": "4.17.21",
"@types/express-session": "1.18.1", "@types/express-session": "1.18.1",
"@types/faker": "5.5.9", "@types/faker": "5.5.9",
"@types/hash-sum": "^1.0.0", "@types/hash-sum": "^1.0.0",
"@types/jest": "29.5.14",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/lodash.groupby": "4.6.9", "@types/lodash.groupby": "4.6.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
@ -226,6 +165,8 @@
"@types/supertest": "6.0.2", "@types/supertest": "6.0.2",
"@types/type-is": "1.6.7", "@types/type-is": "1.6.7",
"@types/uuid": "9.0.8", "@types/uuid": "9.0.8",
"@vitest/coverage-v8": "^3.1.3",
"@vitest/ui": "^3.1.3",
"concurrently": "^9.0.0", "concurrently": "^9.0.0",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"coveralls": "^3.1.1", "coveralls": "^3.1.1",
@ -234,8 +175,6 @@
"fast-check": "3.23.2", "fast-check": "3.23.2",
"fetch-mock": "^12.0.0", "fetch-mock": "^12.0.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"jest": "29.7.0",
"jest-junit": "^16.0.0",
"lint-staged": "15.4.3", "lint-staged": "15.4.3",
"nock": "13.5.6", "nock": "13.5.6",
"openapi-enforcer": "1.23.0", "openapi-enforcer": "1.23.0",
@ -247,6 +186,7 @@
"tsc-watch": "6.2.1", "tsc-watch": "6.2.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite-node": "^3.1.3", "vite-node": "^3.1.3",
"vitest": "^3.1.3",
"wait-on": "^8.0.0" "wait-on": "^8.0.0"
}, },
"resolutions": { "resolutions": {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should create default config 1`] = ` exports[`should create default config 1`] = `
{ {

View File

@ -1,15 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Datadog integration Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project ** \\n %%% ","title":"Unleash notification update"}"`; exports[`Datadog integration > Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project ** \\n %%% ","title":"Unleash notification update"}"`;
exports[`Datadog integration Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Datadog integration > Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)* \\n %%% ","title":"Unleash notification update"}"`;
exports[`Datadog integration Should call datadog webhook 1`] = `"{"text":"%%% \\n *some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project ** \\n %%% ","title":"Unleash notification update"}"`; exports[`Datadog integration > Should call datadog webhook 1`] = `"{"text":"%%% \\n *some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project ** \\n %%% ","title":"Unleash notification update"}"`;
exports[`Datadog integration Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Datadog integration > Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`;
exports[`Datadog integration Should call datadog webhook with JSON when template set 1`] = `"{"text":"{\\n \\"event\\": \\"feature-created\\",\\n \\"createdBy\\": \\"some@user.com\\"\\n}","title":"Unleash notification update"}"`; exports[`Datadog integration > Should call datadog webhook with JSON when template set 1`] = `"{"text":"{\\n \\"event\\": \\"feature-created\\",\\n \\"createdBy\\": \\"some@user.com\\"\\n}","title":"Unleash notification update"}"`;
exports[`Datadog integration Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; exports[`Datadog integration > Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`;
exports[`Datadog integration Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`; exports[`Datadog integration > Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`;

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Should format specialised text for events when IPs changed 1`] = ` exports[`Should format specialised text for events when IPs changed 1`] = `
{ {

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Slack integration Should call slack webhook 1`] = ` exports[`Slack integration > Should call slack webhook 1`] = `
{ {
"attachments": [ "attachments": [
{ {
@ -23,10 +23,10 @@ exports[`Slack integration Should call slack webhook 1`] = `
} }
`; `;
exports[`Slack integration Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects//archive"}]}]}"`; exports[`Slack integration > Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects//archive"}]}]}"`;
exports[`Slack integration Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project *<http://some-url.com/projects/some-project|some-project>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`; exports[`Slack integration > Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* archived *some-toggle* in project *<http://some-url.com/projects/some-project|some-project>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`;
exports[`Slack integration Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; exports[`Slack integration > Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
exports[`Slack integration Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; exports[`Slack integration > Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"*some@user.com* disabled *<http://some-url.com/projects/default/features/some-toggle|some-toggle>* for the *development* environment in project *<http://some-url.com/projects/default|default>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Teams integration Should call teams webhook 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-created"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//features/some-toggle"}]}]}"`; exports[`Teams integration > Should call teams webhook 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-created"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//features/some-toggle"}]}]}"`;
exports[`Teams integration Should call teams webhook for archived toggle 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//archive"}]}]}"`; exports[`Teams integration > Should call teams webhook for archived toggle 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//archive"}]}]}"`;
exports[`Teams integration Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`; exports[`Teams integration > Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`;
exports[`Teams integration Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; exports[`Teams integration > Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
exports[`Teams integration Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; exports[`Teams integration > Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Webhook integration should allow for eventJson and eventMarkdown in bodyTemplate 1`] = ` exports[`Webhook integration > should allow for eventJson and eventMarkdown in bodyTemplate 1`] = `
"{ "{
"json": "{\\"id\\":1,\\"createdAt\\":\\"2024-07-24T00:00:00.000Z\\",\\"createdByUserId\\":-1337,\\"type\\":\\"feature-created\\",\\"createdBy\\":\\"some@user.com\\",\\"featureName\\":\\"some-toggle\\",\\"project\\":\\"default\\",\\"data\\":{\\"name\\":\\"some-toggle\\",\\"enabled\\":false,\\"strategies\\":[{\\"name\\":\\"default\\"}]}}", "json": "{\\"id\\":1,\\"createdAt\\":\\"2024-07-24T00:00:00.000Z\\",\\"createdByUserId\\":-1337,\\"type\\":\\"feature-created\\",\\"createdBy\\":\\"some@user.com\\",\\"featureName\\":\\"some-toggle\\",\\"project\\":\\"default\\",\\"data\\":{\\"name\\":\\"some-toggle\\",\\"enabled\\":false,\\"strategies\\":[{\\"name\\":\\"default\\"}]}}",
"markdown": "*some@user.com* created *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* in project *[default](http://some-url.com/projects/default)*" "markdown": "*some@user.com* created *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* in project *[default](http://some-url.com/projects/default)*"

View File

@ -16,17 +16,17 @@ import {
} from '../types/index.js'; } from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js'; import type { IntegrationEventsService } from '../services/index.js';
import nock from 'nock'; import nock from 'nock';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
const INTEGRATION_ID = 1337; const INTEGRATION_ID = 1337;
const ARGS: IAddonConfig = { const ARGS: IAddonConfig = {
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',
integrationEventsService: { integrationEventsService: {
registerEvent: jest.fn(), registerEvent: vi.fn(),
} as unknown as IntegrationEventsService, } as unknown as IntegrationEventsService,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver, flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: <any>{ emit: jest.fn() }, eventBus: <any>{ emit: vi.fn() },
}; };
describe('Datadog integration', () => { describe('Datadog integration', () => {
@ -317,7 +317,7 @@ describe('Datadog integration', () => {
test('Should call registerEvent', async () => { test('Should call registerEvent', async () => {
const addon = new DatadogAddon(ARGS); const addon = new DatadogAddon(ARGS);
const registerEventSpy = jest.spyOn(addon, 'registerEvent'); const registerEventSpy = vi.spyOn(addon, 'registerEvent');
const event: IEvent = { const event: IEvent = {
id: 1, id: 1,
createdAt: new Date(), createdAt: new Date(),

View File

@ -17,12 +17,12 @@ import {
FEATURE_ARCHIVED, FEATURE_ARCHIVED,
FEATURE_ENVIRONMENT_DISABLED, FEATURE_ENVIRONMENT_DISABLED,
} from '../events/index.js'; } from '../events/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import nock from 'nock'; import nock from 'nock';
const asyncGunzip = promisify(gunzip); const asyncGunzip = promisify(gunzip);
const registerEventMock = jest.fn(); const registerEventMock = vi.fn();
const INTEGRATION_ID = 1337; const INTEGRATION_ID = 1337;
const ARGS: IAddonConfig = { const ARGS: IAddonConfig = {
@ -33,7 +33,7 @@ const ARGS: IAddonConfig = {
} as unknown as IntegrationEventsService, } as unknown as IntegrationEventsService,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver, flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: { eventBus: {
emit: jest.fn(), emit: vi.fn(),
} as any, } as any,
}; };

View File

@ -14,18 +14,18 @@ import {
} from '../types/index.js'; } from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js'; import type { IntegrationEventsService } from '../services/index.js';
const slackApiCalls: ChatPostMessageArguments[] = []; const slackApiCalls: ChatPostMessageArguments[] = [];
import { jest } from '@jest/globals'; import { vi } from 'vitest';
const loggerMock = { const loggerMock = {
debug: jest.fn(), debug: vi.fn(),
info: jest.fn(), info: vi.fn(),
warn: jest.fn(), warn: vi.fn(),
error: jest.fn(), error: vi.fn(),
fatal: jest.fn(), fatal: vi.fn(),
}; };
const getLogger = jest.fn(() => loggerMock); const getLogger = vi.fn(() => loggerMock);
const registerEventMock = jest.fn(); const registerEventMock = vi.fn();
const INTEGRATION_ID = 1337; const INTEGRATION_ID = 1337;
const ARGS: IAddonConfig = { const ARGS: IAddonConfig = {
@ -36,11 +36,11 @@ const ARGS: IAddonConfig = {
} as unknown as IntegrationEventsService, } as unknown as IntegrationEventsService,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver, flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: { eventBus: {
emit: jest.fn(), emit: vi.fn(),
} as any, } as any,
}; };
let postMessage = jest.fn().mockImplementation((options: any) => { let postMessage = vi.fn().mockImplementation((options: any) => {
slackApiCalls.push(options); slackApiCalls.push(options);
return Promise.resolve(); return Promise.resolve();
}); });
@ -73,7 +73,7 @@ describe('SlackAppAddon', () => {
}; };
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); vi.useFakeTimers();
slackApiCalls.length = 0; slackApiCalls.length = 0;
postMessage.mockClear(); postMessage.mockClear();
registerEventMock.mockClear(); registerEventMock.mockClear();
@ -82,13 +82,13 @@ describe('SlackAppAddon', () => {
chat: { chat: {
postMessage, postMessage,
}, },
on: jest.fn(), on: vi.fn(),
} as unknown as Methods; } as unknown as Methods;
}); });
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
it('should post message when feature is toggled', async () => { it('should post message when feature is toggled', async () => {
@ -182,7 +182,7 @@ describe('SlackAppAddon', () => {
it('should log error when an API call fails', async () => { it('should log error when an API call fails', async () => {
// @ts-ignore // @ts-ignore
postMessage = jest.fn().mockRejectedValue(mockError); postMessage = vi.fn().mockRejectedValue(mockError);
await addon.handleEvent( await addon.handleEvent(
event, event,
@ -208,13 +208,10 @@ describe('SlackAppAddon', () => {
], ],
}; };
postMessage = jest postMessage = vi
.fn() .fn()
// @ts-ignore
.mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true })
// @ts-ignore
.mockResolvedValueOnce({ ok: true }) .mockResolvedValueOnce({ ok: true })
// @ts-ignore
.mockRejectedValueOnce(mockError); .mockRejectedValueOnce(mockError);
await addon.handleEvent( await addon.handleEvent(

View File

@ -17,10 +17,10 @@ import {
} from '../types/index.js'; } from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js'; import type { IntegrationEventsService } from '../services/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import nock from 'nock'; import nock from 'nock';
const registerEventMock = jest.fn(); const registerEventMock = vi.fn();
const INTEGRATION_ID = 1337; const INTEGRATION_ID = 1337;
const ARGS: IAddonConfig = { const ARGS: IAddonConfig = {
@ -31,7 +31,7 @@ const ARGS: IAddonConfig = {
} as unknown as IntegrationEventsService, } as unknown as IntegrationEventsService,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver, flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: { eventBus: {
emit: jest.fn(), emit: vi.fn(),
} as any, } as any,
}; };
describe('Slack integration', () => { describe('Slack integration', () => {

View File

@ -17,10 +17,10 @@ import {
} from '../types/index.js'; } from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js'; import type { IntegrationEventsService } from '../services/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import nock from 'nock'; import nock from 'nock';
const registerEventMock = jest.fn(); const registerEventMock = vi.fn();
const INTEGRATION_ID = 1337; const INTEGRATION_ID = 1337;
const ARGS: IAddonConfig = { const ARGS: IAddonConfig = {
@ -31,7 +31,7 @@ const ARGS: IAddonConfig = {
} as unknown as IntegrationEventsService, } as unknown as IntegrationEventsService,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver, flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: { eventBus: {
emit: jest.fn(), emit: vi.fn(),
} as any, } as any,
}; };

View File

@ -12,7 +12,7 @@ import {
} from '../types/index.js'; } from '../types/index.js';
import type { IntegrationEventsService } from '../services/index.js'; import type { IntegrationEventsService } from '../services/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import EventEmitter from 'node:events'; import EventEmitter from 'node:events';
import nock from 'nock'; import nock from 'nock';
@ -28,7 +28,7 @@ afterEach(() => {
}); });
const setup = () => { const setup = () => {
const registerEventMock = jest.fn(); const registerEventMock = vi.fn();
const addonConfig: IAddonConfig = { const addonConfig: IAddonConfig = {
getLogger: noLogger, getLogger: noLogger,
unleashUrl: 'http://some-url.com', unleashUrl: 'http://some-url.com',

View File

@ -1,9 +1,9 @@
import { createTestConfig } from '../test/config/test-config.js'; import { createTestConfig } from '../test/config/test-config.js';
import { jest } from '@jest/globals'; import { type Mock, vi } from 'vitest';
// This mock setup MUST be at the top-level, before any other code that might trigger imports. // This mock setup MUST be at the top-level, before any other code that might trigger imports.
jest.unstable_mockModule('compression', () => ({ vi.mock('compression', () => ({
default: jest.fn().mockImplementation(() => { default: vi.fn().mockImplementation(() => {
// This is the actual middleware function Express would use // This is the actual middleware function Express would use
return (req, res, next) => { return (req, res, next) => {
next(); next();
@ -15,8 +15,8 @@ let compression: any;
const openApiService = { const openApiService = {
// returns a middleware // returns a middleware
validPath: jest.fn().mockReturnValue(() => {}), validPath: vi.fn().mockReturnValue(() => {}),
useDocs: jest.fn(), useDocs: vi.fn(),
}; };
const appModule = await import('./app.js'); const appModule = await import('./app.js');
@ -52,10 +52,10 @@ test('should call preRouterHook', async () => {
describe('compression middleware', () => { describe('compression middleware', () => {
beforeAll(async () => { beforeAll(async () => {
jest.resetModules(); // Crucial: Clears the module cache. vi.resetModules(); // Crucial: Clears the module cache.
// Import 'compression' AFTER resetModules. This ensures we get the mock. // Import 'compression' AFTER resetModules. This ensures we get the mock.
const compressionModule = await import('compression'); const compressionModule = await import('compression');
compression = compressionModule.default; // `compression` is now our jest.fn() compression = compressionModule.default; // `compression` is now our vi.fn()
// Import 'app.js' AFTER resetModules and AFTER 'compression' is set to the mock. // Import 'app.js' AFTER resetModules and AFTER 'compression' is set to the mock.
// This ensures app.js uses the mocked version of compression. // This ensures app.js uses the mocked version of compression.
@ -67,9 +67,9 @@ describe('compression middleware', () => {
// Clear call history for the mock before each test in this describe block // Clear call history for the mock before each test in this describe block
if ( if (
compression && compression &&
typeof (compression as jest.Mock).mockClear === 'function' typeof (compression as Mock).mockClear === 'function'
) { ) {
(compression as jest.Mock).mockClear(); (compression as Mock).mockClear();
} else { } else {
// This case might happen if beforeAll failed or types are unexpected // This case might happen if beforeAll failed or types are unexpected
console.warn( console.warn(

View File

@ -5,6 +5,7 @@ import type { AccessStore } from './access-store.js';
import { BadDataError } from '../error/index.js'; import { BadDataError } from '../error/index.js';
let db: ITestDb; let db: ITestDb;
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('access_store_serial', getLogger); db = await dbInit('access_store_serial', getLogger);

View File

@ -5,10 +5,10 @@ import {
throwExceedsLimitError, throwExceedsLimitError,
} from './exceeds-limit-error.js'; } from './exceeds-limit-error.js';
import { jest } from '@jest/globals'; import { vi, it, expect } from 'vitest';
it('emits events event when created through the external function', () => { it('emits events event when created through the external function', () => {
const emitEvent = jest.fn(); const emitEvent = vi.fn();
const resource = 'some-resource'; const resource = 'some-resource';
const limit = 10; const limit = 10;
@ -31,7 +31,7 @@ it('emits events event when created through the external function', () => {
}); });
it('emits uses the resourceNameOverride for the event when provided, but uses the resource for the error', () => { it('emits uses the resourceNameOverride for the event when provided, but uses the resource for the error', () => {
const emitEvent = jest.fn(); const emitEvent = vi.fn();
const resource = 'not this'; const resource = 'not this';
const resourceNameOverride = 'but this!'; const resourceNameOverride = 'but this!';
const limit = 10; const limit = 10;
@ -47,7 +47,9 @@ it('emits uses the resourceNameOverride for the event when provided, but uses th
limit, limit,
}, },
), ),
).toThrow(new ExceedsLimitError(resource, limit)); ).toThrowError(
expect.errorWithMessage(new ExceedsLimitError(resource, limit)),
);
expect(emitEvent).toHaveBeenCalledWith(EXCEEDS_LIMIT, { expect(emitEvent).toHaveBeenCalledWith(EXCEEDS_LIMIT, {
resource: resourceNameOverride, resource: resourceNameOverride,

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should match snapshot from /api/client/features 1`] = ` exports[`should match snapshot from /api/client/features 1`] = `
{ {

View File

@ -11,7 +11,7 @@ import type { Application } from 'express';
import type { IFlagResolver } from '../../../types/index.js'; import type { IFlagResolver } from '../../../types/index.js';
import type TestAgent from 'supertest/lib/agent.d.ts'; import type TestAgent from 'supertest/lib/agent.d.ts';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let app: Application; let app: Application;
@ -78,10 +78,10 @@ test('should get empty getFeatures via client', () => {
}); });
test('if caching is enabled should memoize', async () => { test('if caching is enabled should memoize', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]); const getClientFeatures = vi.fn().mockReturnValue([]);
const getActiveSegmentsForClient = jest.fn().mockReturnValue([]); const getActiveSegmentsForClient = vi.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({}); const respondWithValidation = vi.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn()); const validPath = vi.fn().mockReturnValue(vi.fn());
const clientSpecService = new ClientSpecService({ getLogger }); const clientSpecService = new ClientSpecService({ getLogger });
const openApiService = { respondWithValidation, validPath }; const openApiService = { respondWithValidation, validPath };
const clientFeatureToggleService = { const clientFeatureToggleService = {
@ -119,10 +119,10 @@ test('if caching is enabled should memoize', async () => {
}); });
test('if caching is not enabled all calls goes to service', async () => { test('if caching is not enabled all calls goes to service', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]); const getClientFeatures = vi.fn().mockReturnValue([]);
const getActiveSegmentsForClient = jest.fn().mockReturnValue([]); const getActiveSegmentsForClient = vi.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({}); const respondWithValidation = vi.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn()); const validPath = vi.fn().mockReturnValue(vi.fn());
const clientSpecService = new ClientSpecService({ getLogger }); const clientSpecService = new ClientSpecService({ getLogger });
const clientFeatureToggleService = { const clientFeatureToggleService = {
getClientFeatures, getClientFeatures,

View File

@ -11,7 +11,7 @@ import type { ProjectAccess } from '../private-project/privateProjectStore.js';
import EventService, { filterAccessibleProjects } from './event-service.js'; import EventService, { filterAccessibleProjects } from './event-service.js';
import { type IBaseEvent, USER_UPDATED } from '../../events/index.js'; import { type IBaseEvent, USER_UPDATED } from '../../events/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
describe('filterPrivateProjectsFromParams', () => { describe('filterPrivateProjectsFromParams', () => {
it('should return IS_ANY_OF with allowed projects when projectParam is undefined and mode is limited', () => { it('should return IS_ANY_OF with allowed projects when projectParam is undefined and mode is limited', () => {
@ -124,13 +124,13 @@ describe('storeEvents', () => {
'should store the event %s', 'should store the event %s',
async (preDataAndData: Pick<IBaseEvent, 'preData' | 'data'>) => { async (preDataAndData: Pick<IBaseEvent, 'preData' | 'data'>) => {
const eventStore = { const eventStore = {
batchStore: jest.fn(), batchStore: vi.fn(),
} as unknown as IEventStore; } as unknown as IEventStore;
const eventService = new EventService( const eventService = new EventService(
{ {
eventStore, eventStore,
featureTagStore: { featureTagStore: {
getAllByFeatures: jest.fn().mockReturnValue([]), getAllByFeatures: vi.fn().mockReturnValue([]),
} as unknown as IFeatureTagStore, } as unknown as IFeatureTagStore,
}, },
{ getLogger, eventBus: undefined } as unknown as IUnleashConfig, { getLogger, eventBus: undefined } as unknown as IUnleashConfig,
@ -152,13 +152,13 @@ describe('storeEvents', () => {
); );
test('should not store the event when predata and data are the same', async () => { test('should not store the event when predata and data are the same', async () => {
const eventStore = { const eventStore = {
batchStore: jest.fn(), batchStore: vi.fn(),
} as unknown as IEventStore; } as unknown as IEventStore;
const eventService = new EventService( const eventService = new EventService(
{ {
eventStore, eventStore,
featureTagStore: { featureTagStore: {
getAllByFeatures: jest.fn().mockReturnValue([]), getAllByFeatures: vi.fn().mockReturnValue([]),
} as unknown as IFeatureTagStore, } as unknown as IFeatureTagStore,
}, },
{ getLogger, eventBus: undefined } as unknown as IUnleashConfig, { getLogger, eventBus: undefined } as unknown as IUnleashConfig,

View File

@ -99,7 +99,7 @@ describe('validate incoming feature naming data', () => {
}); });
if (result.state === 'valid') { if (result.state === 'valid') {
fail('Expected invalid result'); expect.fail('Expected invalid result');
} }
expect(result.reasons.length).toBe(1); expect(result.reasons.length).toBe(1);

View File

@ -32,7 +32,14 @@ import {
import { insertLastSeenAt } from '../../../../test/e2e/helpers/test-helper.js'; import { insertLastSeenAt } from '../../../../test/e2e/helpers/test-helper.js';
import type { EventService } from '../../../services/index.js'; import type { EventService } from '../../../services/index.js';
import type FeatureLinkService from '../../feature-links/feature-link-service.js'; import type FeatureLinkService from '../../feature-links/feature-link-service.js';
import {
beforeAll,
afterAll,
beforeEach,
test,
expect,
describe,
} from 'vitest';
let stores: IUnleashStores; let stores: IUnleashStores;
let db: ITestDb; let db: ITestDb;
let service: FeatureToggleService; let service: FeatureToggleService;
@ -385,7 +392,7 @@ test('cloning a feature flag not allowed for change requests enabled', async ()
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
true, true,
), ),
).rejects.toEqual( ).rejects.errorWithMessage(
new ForbiddenError( new ForbiddenError(
`Cloning not allowed. Project default has change requests enabled.`, `Cloning not allowed. Project default has change requests enabled.`,
), ),
@ -399,7 +406,7 @@ test('changing to a project with change requests enabled should not be allowed',
}); });
await expect( await expect(
service.changeProject('newFlagName', 'default', TEST_AUDIT_USER), service.changeProject('newFlagName', 'default', TEST_AUDIT_USER),
).rejects.toEqual( ).rejects.errorWithMessage(
new ForbiddenError( new ForbiddenError(
`Changing project not allowed. Project default has change requests enabled.`, `Changing project not allowed. Project default has change requests enabled.`,
), ),
@ -531,7 +538,9 @@ test('If change requests are enabled, cannot change variants without going via C
TEST_AUDIT_USER, TEST_AUDIT_USER,
[], [],
), ),
).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); ).rejects.toThrowError(
expect.errorWithMessage(new PermissionError(SKIP_CHANGE_REQUEST)),
);
}); });
test('If CRs are protected for any environment in the project stops bulk update of variants', async () => { test('If CRs are protected for any environment in the project stops bulk update of variants', async () => {
@ -620,7 +629,9 @@ test('If CRs are protected for any environment in the project stops bulk update
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
), ),
).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); ).rejects.toThrowError(
expect.errorWithMessage(new PermissionError(SKIP_CHANGE_REQUEST)),
);
}); });
test('getPlaygroundFeatures should return ids and titles (if they exist) on client strategies', async () => { test('getPlaygroundFeatures should return ids and titles (if they exist) on client strategies', async () => {
@ -799,7 +810,7 @@ test('Should not allow to add flags to archived projects', async () => {
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
), ),
).rejects.toEqual( ).rejects.errorWithMessage(
new NotFoundError( new NotFoundError(
`Active project with id archivedProject does not exist`, `Active project with id archivedProject does not exist`,
), ),
@ -828,7 +839,7 @@ test('Should not allow to revive flags to archived projects', async () => {
await expect( await expect(
service.reviveFeature(flag.name, TEST_AUDIT_USER), service.reviveFeature(flag.name, TEST_AUDIT_USER),
).rejects.toEqual( ).rejects.errorWithMessage(
new NotFoundError( new NotFoundError(
`Active project with id archivedProjectWithFlag does not exist`, `Active project with id archivedProjectWithFlag does not exist`,
), ),
@ -836,7 +847,7 @@ test('Should not allow to revive flags to archived projects', async () => {
await expect( await expect(
service.reviveFeatures([flag.name], project.id, TEST_AUDIT_USER), service.reviveFeatures([flag.name], project.id, TEST_AUDIT_USER),
).rejects.toEqual( ).rejects.errorWithMessage(
new NotFoundError( new NotFoundError(
`Active project with id archivedProjectWithFlag does not exist`, `Active project with id archivedProjectWithFlag does not exist`,
), ),

View File

@ -9,7 +9,7 @@ import type {
} from '../../../types/index.js'; } from '../../../types/index.js';
import getLogger from '../../../../test/fixtures/no-logger.js'; import getLogger from '../../../../test/fixtures/no-logger.js';
import { ExceedsLimitError } from '../../../error/exceeds-limit-error.js'; import { ExceedsLimitError } from '../../../error/exceeds-limit-error.js';
import { describe, test, expect } from 'vitest';
const alwaysOnFlagResolver = { const alwaysOnFlagResolver = {
isEnabled() { isEnabled() {
return true; return true;
@ -43,7 +43,7 @@ describe('Strategy limits', () => {
await addStrategy(); await addStrategy();
} }
await expect(addStrategy()).rejects.toThrow( await expect(addStrategy()).rejects.toThrowError(
"Failed to create strategy. You can't create more than the established limit of 3", "Failed to create strategy. You can't create more than the established limit of 3",
); );
}); });
@ -87,7 +87,7 @@ describe('Strategy limits', () => {
contextName: 'accountId', contextName: 'accountId',
}, },
]), ]),
).rejects.toThrow( ).rejects.toThrowError(
"Failed to create constraints. You can't create more than the established limit of 1", "Failed to create constraints. You can't create more than the established limit of 1",
); );
}); });
@ -159,7 +159,7 @@ describe('Strategy limits', () => {
// check that you can't save more constraints // check that you can't save more constraints
await expect(async () => await expect(async () =>
updateStrategy([...constraints, ...constraints]), updateStrategy([...constraints, ...constraints]),
).rejects.toThrow(new ExceedsLimitError('constraints', LIMIT)); ).rejects.errorWithMessage(new ExceedsLimitError('constraints', LIMIT));
}); });
test('Should not allow to exceed constraint values limit', async () => { test('Should not allow to exceed constraint values limit', async () => {
@ -195,7 +195,7 @@ describe('Strategy limits', () => {
values: ['1', '2', '3', '4'], values: ['1', '2', '3', '4'],
}, },
]), ]),
).rejects.toThrow( ).rejects.toThrowError(
"Failed to create constraint values for userId. You can't create more than the established limit of 3", "Failed to create constraint values for userId. You can't create more than the established limit of 3",
); );
}); });
@ -266,7 +266,7 @@ describe('Strategy limits', () => {
// check that you can't save more constraint values // check that you can't save more constraint values
await expect(async () => await expect(async () =>
updateStrategy(initialConstraintValueCount + 1), updateStrategy(initialConstraintValueCount + 1),
).rejects.toThrow( ).rejects.errorWithMessage(
new ExceedsLimitError('constraint values for appName', LIMIT), new ExceedsLimitError('constraint values for appName', LIMIT),
); );
}); });

View File

@ -33,7 +33,7 @@ import type {
SetStrategySortOrderSchema, SetStrategySortOrderSchema,
} from '../../../openapi/index.js'; } from '../../../openapi/index.js';
import { ForbiddenError } from '../../../error/index.js'; import { ForbiddenError } from '../../../error/index.js';
import { beforeAll, afterEach, afterAll, test, describe, expect } from 'vitest';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
let defaultToken: IApiToken; let defaultToken: IApiToken;
@ -282,7 +282,7 @@ test('should not allow to change project with dependencies', async () => {
'default', 'default',
TEST_AUDIT_USER, TEST_AUDIT_USER,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new ForbiddenError( new ForbiddenError(
'Changing project not allowed. Feature has dependencies.', 'Changing project not allowed. Feature has dependencies.',
), ),
@ -2329,7 +2329,7 @@ test('Should not allow changing project to target project without the same enabl
'default', 'default',
TEST_AUDIT_USER, TEST_AUDIT_USER,
), ),
).rejects.toThrow(new IncompatibleProjectError(targetProject)); ).rejects.errorWithMessage(new IncompatibleProjectError(targetProject));
}); });
test('Should allow changing project to target project with the same enabled environments', async () => { test('Should allow changing project to target project with the same enabled environments', async () => {
@ -2401,7 +2401,7 @@ test('Should allow changing project to target project with the same enabled envi
environment: '*', environment: '*',
secret: 'a', secret: 'a',
}); });
await expect(async () => await expect(
app.services.projectService.changeProject( app.services.projectService.changeProject(
targetProject, targetProject,
featureName, featureName,

View File

@ -9,7 +9,7 @@ import getLogger from '../../../test/fixtures/no-logger.js';
import { randomId } from '../../util/index.js'; import { randomId } from '../../util/index.js';
import { ApiTokenType } from '../../types/model.js'; import { ApiTokenType } from '../../types/model.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let app: IUnleashNoSupertest; let app: IUnleashNoSupertest;
let db: ITestDb; let db: ITestDb;
@ -37,7 +37,7 @@ beforeAll(async () => {
afterEach(() => { afterEach(() => {
app.services.frontendApiService.stopAll(); app.services.frontendApiService.stopAll();
jest.clearAllMocks(); vi.clearAllMocks();
}); });
afterAll(async () => { afterAll(async () => {

View File

@ -21,7 +21,7 @@ import {
} from '../../types/index.js'; } from '../../types/index.js';
import type { FrontendApiService } from './frontend-api-service.js'; import type { FrontendApiService } from './frontend-api-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -42,7 +42,7 @@ beforeAll(async () => {
afterEach(() => { afterEach(() => {
app.services.frontendApiService.stopAll(); app.services.frontendApiService.stopAll();
jest.clearAllMocks(); vi.clearAllMocks();
}); });
afterAll(async () => { afterAll(async () => {
@ -362,7 +362,7 @@ test('should store frontend api client metrics', async () => {
}); });
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2 // @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
app.services.clientMetricsServiceV2.cachedFeatureNames = jest app.services.clientMetricsServiceV2.cachedFeatureNames = vi
.fn<() => Promise<string[]>>() .fn<() => Promise<string[]>>()
.mockResolvedValue([featureName]); .mockResolvedValue([featureName]);

View File

@ -14,7 +14,7 @@ import type {
} from '../../types/index.js'; } from '../../types/index.js';
import { UPDATE_REVISION } from '../feature-toggle/configuration-revision-service.js'; import { UPDATE_REVISION } from '../feature-toggle/configuration-revision-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
const state = async ( const state = async (
cache: GlobalFrontendApiCache, cache: GlobalFrontendApiCache,
@ -53,7 +53,7 @@ const createCache = (
const config = { const config = {
getLogger: noLogger, getLogger: noLogger,
flagResolver: alwaysOnFlagResolver, flagResolver: alwaysOnFlagResolver,
eventBus: <any>{ emit: jest.fn() }, eventBus: <any>{ emit: vi.fn() },
}; };
const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]); const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]);
const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel( const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel(

View File

@ -12,7 +12,7 @@ import type {
IUnleashStores, IUnleashStores,
} from '../../types/index.js'; } from '../../types/index.js';
import { createFakeGetLicensedUsers } from './getLicensedUsers.js'; import { createFakeGetLicensedUsers } from './getLicensedUsers.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let instanceStatsService: InstanceStatsService; let instanceStatsService: InstanceStatsService;
let versionService: VersionService; let versionService: VersionService;
@ -22,7 +22,7 @@ let flagResolver: IFlagResolver;
let updateMetrics: () => Promise<void>; let updateMetrics: () => Promise<void>;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
register.clear(); register.clear();
@ -49,8 +49,8 @@ beforeEach(() => {
); );
updateMetrics = collectAggDbMetrics; updateMetrics = collectAggDbMetrics;
jest.spyOn(clientInstanceStore, 'getDistinctApplicationsCount'); vi.spyOn(clientInstanceStore, 'getDistinctApplicationsCount');
jest.spyOn(instanceStatsService, 'getStats'); vi.spyOn(instanceStatsService, 'getStats');
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0); expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
}); });
@ -84,7 +84,7 @@ describe.each([true, false])(
'When feature enabled is %s', 'When feature enabled is %s',
(featureEnabled: boolean) => { (featureEnabled: boolean) => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(flagResolver, 'getVariant').mockReturnValue({ vi.spyOn(flagResolver, 'getVariant').mockReturnValue({
name: 'memorizeStats', name: 'memorizeStats',
enabled: featureEnabled, enabled: featureEnabled,
feature_enabled: featureEnabled, feature_enabled: featureEnabled,
@ -93,9 +93,7 @@ describe.each([true, false])(
test(`should${featureEnabled ? ' ' : ' not '}memoize query results`, async () => { test(`should${featureEnabled ? ' ' : ' not '}memoize query results`, async () => {
const segmentStore = stores.segmentStore; const segmentStore = stores.segmentStore;
jest.spyOn(segmentStore, 'count').mockReturnValue( vi.spyOn(segmentStore, 'count').mockReturnValue(Promise.resolve(5));
Promise.resolve(5),
);
expect(segmentStore.count).toHaveBeenCalledTimes(0); expect(segmentStore.count).toHaveBeenCalledTimes(0);
expect(await instanceStatsService.segmentCount()).toBe(5); expect(await instanceStatsService.segmentCount()).toBe(5);
expect(segmentStore.count).toHaveBeenCalledTimes(1); expect(segmentStore.count).toHaveBeenCalledTimes(1);
@ -107,7 +105,7 @@ describe.each([true, false])(
test(`should${featureEnabled ? ' ' : ' not '}memoize async query results`, async () => { test(`should${featureEnabled ? ' ' : ' not '}memoize async query results`, async () => {
const trafficDataUsageStore = stores.trafficDataUsageStore; const trafficDataUsageStore = stores.trafficDataUsageStore;
jest.spyOn( vi.spyOn(
trafficDataUsageStore, trafficDataUsageStore,
'getTrafficDataUsageForPeriod', 'getTrafficDataUsageForPeriod',
).mockReturnValue( ).mockReturnValue(
@ -142,7 +140,7 @@ describe.each([true, false])(
test(`getStats should${featureEnabled ? ' ' : ' not '}be memorized`, async () => { test(`getStats should${featureEnabled ? ' ' : ' not '}be memorized`, async () => {
const featureStrategiesReadModel = const featureStrategiesReadModel =
stores.featureStrategiesReadModel; stores.featureStrategiesReadModel;
jest.spyOn( vi.spyOn(
featureStrategiesReadModel, featureStrategiesReadModel,
'getMaxFeatureEnvironmentStrategies', 'getMaxFeatureEnvironmentStrategies',
).mockReturnValue( ).mockReturnValue(

View File

@ -5,7 +5,7 @@ import { createTestConfig } from '../../../test/config/test-config.js';
import FakeSettingStore from '../../../test/fixtures/fake-setting-store.js'; import FakeSettingStore from '../../../test/fixtures/fake-setting-store.js';
import type EventService from '../events/event-service.js'; import type EventService from '../events/event-service.js';
import { TEST_AUDIT_USER } from '../../types/index.js'; import { TEST_AUDIT_USER } from '../../types/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
test('Scheduler should run scheduled functions if maintenance mode is off', async () => { test('Scheduler should run scheduled functions if maintenance mode is off', async () => {
const config = createTestConfig(); const config = createTestConfig();
@ -20,7 +20,7 @@ test('Scheduler should run scheduled functions if maintenance mode is off', asyn
config.eventBus, config.eventBus,
); );
const job = jest.fn(); const job = vi.fn();
await schedulerService.schedule( await schedulerService.schedule(
async () => { async () => {
@ -52,7 +52,7 @@ test('Scheduler should not run scheduled functions if maintenance mode is on', a
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
const job = jest.fn(); const job = vi.fn();
await schedulerService.schedule( await schedulerService.schedule(
async () => { async () => {

View File

@ -13,13 +13,13 @@ import { endOfDay, startOfHour, subDays, subHours } from 'date-fns';
import type { IClientMetricsEnv } from './client-metrics-store-v2-type.js'; import type { IClientMetricsEnv } from './client-metrics-store-v2-type.js';
import { UnknownFlagsService } from '../unknown-flags/unknown-flags-service.js'; import { UnknownFlagsService } from '../unknown-flags/unknown-flags-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
function initClientMetrics() { function initClientMetrics() {
const stores = createStores(); const stores = createStores();
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
eventBus.emit = jest.fn(() => true); eventBus.emit = vi.fn(() => true);
const config = { const config = {
eventBus, eventBus,
@ -35,7 +35,7 @@ function initClientMetrics() {
}, },
config, config,
); );
lastSeenService.updateLastSeen = jest.fn(); lastSeenService.updateLastSeen = vi.fn();
const unknownFlagsService = new UnknownFlagsService( const unknownFlagsService = new UnknownFlagsService(
{ {
unknownFlagsStore: stores.unknownFlagsStore, unknownFlagsStore: stores.unknownFlagsStore,
@ -56,7 +56,7 @@ test('process metrics properly', async () => {
const { clientMetricsService, eventBus, lastSeenService, stores } = const { clientMetricsService, eventBus, lastSeenService, stores } =
initClientMetrics(); initClientMetrics();
stores.clientMetricsStoreV2.getFeatureFlagNames = jest stores.clientMetricsStoreV2.getFeatureFlagNames = vi
.fn<() => Promise<string[]>>() .fn<() => Promise<string[]>>()
.mockResolvedValue(['myCoolToggle', 'myOtherToggle']); .mockResolvedValue(['myCoolToggle', 'myOtherToggle']);

View File

@ -12,15 +12,15 @@ import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store
import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store.js'; import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store.js';
import type { IApplicationOverview } from './models.js'; import type { IApplicationOverview } from './models.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let config: IUnleashConfig; let config: IUnleashConfig;
beforeAll(() => { beforeAll(() => {
config = createTestConfig({}); config = createTestConfig({});
}); });
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => { test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
const appStoreSpy = jest.fn(); const appStoreSpy = vi.fn();
const bulkSpy = jest.fn(); const bulkSpy = vi.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
@ -65,12 +65,12 @@ test('Multiple registrations of same appname and instanceid within same time per
expect(registrations[0].started).toBe(client1.started); expect(registrations[0].started).toBe(client1.started);
expect(registrations[0].interval).toBe(client1.interval); expect(registrations[0].interval).toBe(client1.interval);
jest.useRealTimers(); vi.useRealTimers();
}); });
test('Multiple unique clients causes multiple registrations', async () => { test('Multiple unique clients causes multiple registrations', async () => {
const appStoreSpy = jest.fn(); const appStoreSpy = vi.fn();
const bulkSpy = jest.fn(); const bulkSpy = vi.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
@ -119,8 +119,8 @@ test('Multiple unique clients causes multiple registrations', async () => {
}); });
test('Same client registered outside of dedup interval will be registered twice', async () => { test('Same client registered outside of dedup interval will be registered twice', async () => {
const appStoreSpy = jest.fn(); const appStoreSpy = vi.fn();
const bulkSpy = jest.fn(); const bulkSpy = vi.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
@ -162,9 +162,7 @@ test('Same client registered outside of dedup interval will be registered twice'
expect(appStoreSpy).toHaveBeenCalledTimes(2); expect(appStoreSpy).toHaveBeenCalledTimes(2);
expect(bulkSpy).toHaveBeenCalledTimes(2); expect(bulkSpy).toHaveBeenCalledTimes(2);
// @ts-expect-error unknown type
const firstRegistrations = appStoreSpy.mock.calls[0][0][0]; const firstRegistrations = appStoreSpy.mock.calls[0][0][0];
// @ts-expect-error unknown type
const secondRegistrations = appStoreSpy.mock.calls[1][0][0]; const secondRegistrations = appStoreSpy.mock.calls[1][0][0];
expect(firstRegistrations.appName).toBe(secondRegistrations.appName); expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
@ -172,8 +170,8 @@ test('Same client registered outside of dedup interval will be registered twice'
}); });
test('No registrations during a time period will not call stores', async () => { test('No registrations during a time period will not call stores', async () => {
const appStoreSpy = jest.fn(); const appStoreSpy = vi.fn();
const bulkSpy = jest.fn(); const bulkSpy = vi.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };

View File

@ -3,13 +3,13 @@ import EventEmitter from 'events';
import getLogger from '../../../../../test/fixtures/no-logger.js'; import getLogger from '../../../../../test/fixtures/no-logger.js';
import type { IUnleashConfig } from '../../../../types/index.js'; import type { IUnleashConfig } from '../../../../types/index.js';
import { type LastSeenInput, LastSeenService } from '../last-seen-service.js'; import { type LastSeenInput, LastSeenService } from '../last-seen-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
function initLastSeenService(flagEnabled = true) { function initLastSeenService(flagEnabled = true) {
const stores = createStores(); const stores = createStores();
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
eventBus.emit = jest.fn() as () => boolean; eventBus.emit = vi.fn() as () => boolean;
const config = { const config = {
eventBus, eventBus,
@ -38,7 +38,7 @@ function initLastSeenService(flagEnabled = true) {
test('should not add duplicates per feature/environment', async () => { test('should not add duplicates per feature/environment', async () => {
const { lastSeenService, featureToggleStore, lastSeenStore } = const { lastSeenService, featureToggleStore, lastSeenStore } =
initLastSeenService(false); initLastSeenService(false);
const lastSeenSpy = jest.spyOn(lastSeenStore, 'setLastSeen'); const lastSeenSpy = vi.spyOn(lastSeenStore, 'setLastSeen');
lastSeenService.updateLastSeen([ lastSeenService.updateLastSeen([
{ {
@ -95,7 +95,7 @@ test('should call last seen at store with correct data', async () => {
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
lastSeenStore.setLastSeen = jest.fn() as ( lastSeenStore.setLastSeen = vi.fn() as (
data: LastSeenInput[], data: LastSeenInput[],
) => Promise<void>; ) => Promise<void>;
await lastSeenService.store(); await lastSeenService.store();

View File

@ -12,7 +12,7 @@ import { createTestConfig } from '../../../test/config/test-config.js';
import { createOnboardingService } from './createOnboardingService.js'; import { createOnboardingService } from './createOnboardingService.js';
import type EventEmitter from 'events'; import type EventEmitter from 'events';
import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events.js'; import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let db: ITestDb; let db: ITestDb;
let stores: IUnleashStores; let stores: IUnleashStores;
@ -41,25 +41,25 @@ beforeEach(async () => {
await stores.projectStore.deleteAll(); await stores.projectStore.deleteAll();
await stores.onboardingStore.deleteAll(); await stores.onboardingStore.deleteAll();
await stores.userStore.deleteAll(); await stores.userStore.deleteAll();
jest.useRealTimers(); vi.useRealTimers();
}); });
test('Default project should take first user created instead of project created as start time', async () => { test('Default project should take first user created instead of project created as start time', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
jest.setSystemTime(new Date()); vi.setSystemTime(new Date());
const { userStore, featureToggleStore, projectStore } = stores; const { userStore, featureToggleStore, projectStore } = stores;
// default projects are created in advance and should be ignored // default projects are created in advance and should be ignored
await projectStore.create({ id: 'default', name: 'irrelevant' }); await projectStore.create({ id: 'default', name: 'irrelevant' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
const user = await userStore.insert({}); const user = await userStore.insert({});
await featureToggleStore.create('default', { await featureToggleStore.create('default', {
name: 'test-default', name: 'test-default',
createdByUserId: user.id, createdByUserId: user.id,
}); });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ await onboardingService.insert({
type: 'flag-created', type: 'flag-created',
flag: 'test-default', flag: 'test-default',
@ -82,12 +82,12 @@ test('Default project should take first user created instead of project created
}); });
test('Ignore events for existing customers', async () => { test('Ignore events for existing customers', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
jest.setSystemTime(new Date(2024, 8, 2)); // day before we added metrics vi.setSystemTime(new Date(2024, 8, 2)); // day before we added metrics
const { userStore } = stores; const { userStore } = stores;
await userStore.insert({}); await userStore.insert({});
jest.setSystemTime(new Date()); vi.setSystemTime(new Date());
await onboardingService.insert({ type: 'first-user-login' }); await onboardingService.insert({ type: 'first-user-login' });
const { rows: instanceEvents } = await db.rawDatabase.raw( const { rows: instanceEvents } = await db.rawDatabase.raw(
@ -109,8 +109,8 @@ test('Ignore system user in onboarding events', async () => {
}); });
test('Storing onboarding events', async () => { test('Storing onboarding events', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
jest.setSystemTime(new Date()); vi.setSystemTime(new Date());
const { userStore, featureToggleStore, projectStore } = stores; const { userStore, featureToggleStore, projectStore } = stores;
const user = await userStore.insert({}); const user = await userStore.insert({});
await projectStore.create({ id: 'test_project', name: 'irrelevant' }); await projectStore.create({ id: 'test_project', name: 'irrelevant' });
@ -119,23 +119,23 @@ test('Storing onboarding events', async () => {
createdByUserId: user.id, createdByUserId: user.id,
}); });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'first-user-login' }); await onboardingService.insert({ type: 'first-user-login' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'second-user-login' }); await onboardingService.insert({ type: 'second-user-login' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'flag-created', flag: 'test' }); await onboardingService.insert({ type: 'flag-created', flag: 'test' });
await onboardingService.insert({ type: 'flag-created', flag: 'test' }); await onboardingService.insert({ type: 'flag-created', flag: 'test' });
await onboardingService.insert({ type: 'flag-created', flag: 'invalid' }); await onboardingService.insert({ type: 'flag-created', flag: 'invalid' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'pre-live', flag: 'test' }); await onboardingService.insert({ type: 'pre-live', flag: 'test' });
await onboardingService.insert({ type: 'pre-live', flag: 'test' }); await onboardingService.insert({ type: 'pre-live', flag: 'test' });
await onboardingService.insert({ type: 'pre-live', flag: 'invalid' }); await onboardingService.insert({ type: 'pre-live', flag: 'invalid' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'live', flag: 'test' }); await onboardingService.insert({ type: 'live', flag: 'test' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'live', flag: 'test' }); await onboardingService.insert({ type: 'live', flag: 'test' });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
await onboardingService.insert({ type: 'live', flag: 'invalid' }); await onboardingService.insert({ type: 'live', flag: 'invalid' });
const { rows: instanceEvents } = await db.rawDatabase.raw( const { rows: instanceEvents } = await db.rawDatabase.raw(
@ -174,8 +174,8 @@ const reachedOnboardingEvents = (count: number) => {
}; };
test('Reacting to events', async () => { test('Reacting to events', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
jest.setSystemTime(new Date()); vi.setSystemTime(new Date());
const { userStore, featureToggleStore, projectStore } = stores; const { userStore, featureToggleStore, projectStore } = stores;
const user = await userStore.insert({}); const user = await userStore.insert({});
await projectStore.create({ id: 'test_project', name: 'irrelevant' }); await projectStore.create({ id: 'test_project', name: 'irrelevant' });
@ -183,7 +183,7 @@ test('Reacting to events', async () => {
name: 'test', name: 'test',
createdByUserId: user.id, createdByUserId: user.id,
}); });
jest.advanceTimersByTime(minutesToMilliseconds(1)); vi.advanceTimersByTime(minutesToMilliseconds(1));
eventBus.emit(USER_LOGIN, { loginOrder: 0 }); eventBus.emit(USER_LOGIN, { loginOrder: 0 });
eventBus.emit(USER_LOGIN, { loginOrder: 1 }); eventBus.emit(USER_LOGIN, { loginOrder: 1 });

View File

@ -3,7 +3,8 @@ import fc from 'fast-check';
import supertest from 'supertest'; import supertest from 'supertest';
import { createServices } from '../../services/index.js'; import { createServices } from '../../services/index.js';
import { createTestConfig } from '../../../test/config/test-config.js'; import { createTestConfig } from '../../../test/config/test-config.js';
import { it } from '@fast-check/vitest';
import { describe } from 'vitest';
import createStores from '../../../test/fixtures/store.js'; import createStores from '../../../test/fixtures/store.js';
import getApp from '../../app.js'; import getApp from '../../app.js';
@ -80,7 +81,7 @@ describe('the playground API', () => {
.send(payload) .send(payload)
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
return status === 400; expect(status).toBe(400);
}, },
), ),
testParams, testParams,

View File

@ -12,7 +12,7 @@ import {
import NameExistsError from '../../error/name-exists-error.js'; import NameExistsError from '../../error/name-exists-error.js';
import type { EventService } from '../../services/index.js'; import type { EventService } from '../../services/index.js';
import { createEventsService } from '../events/createEventsService.js'; import { createEventsService } from '../events/createEventsService.js';
import { test, beforeAll, afterAll, expect } from 'vitest';
let stores: IUnleashStores; let stores: IUnleashStores;
let db: ITestDb; let db: ITestDb;
let service: EnvironmentService; let service: EnvironmentService;
@ -219,25 +219,26 @@ test('Adding same environment twice should throw a NameExistsError', async () =>
'default', 'default',
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new NameExistsError( new NameExistsError(
'default already has the environment uniqueness-test enabled', 'default already has the environment uniqueness-test enabled',
), ),
); );
}); });
test('Removing environment not connected to project should be a noop', async () => test('Removing environment not connected to project should be a noop', async () => {
expect(async () => await expect(
service.removeEnvironmentFromProject( service.removeEnvironmentFromProject(
'some-non-existing-environment', 'some-non-existing-environment',
'default', 'default',
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
), ),
).resolves); ).resolves;
});
test('Trying to get an environment that does not exist throws NotFoundError', async () => { test('Trying to get an environment that does not exist throws NotFoundError', async () => {
const envName = 'this-should-not-exist'; const envName = 'this-should-not-exist';
await expect(async () => service.get(envName)).rejects.toThrow( await expect(async () => service.get(envName)).rejects.errorWithMessage(
new NotFoundError(`Could not find environment with name: ${envName}`), new NotFoundError(`Could not find environment with name: ${envName}`),
); );
}); });

View File

@ -34,7 +34,14 @@ import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../util/index.js';
import { ApiTokenType } from '../../types/model.js'; import { ApiTokenType } from '../../types/model.js';
import { createApiTokenService } from '../api-tokens/createApiTokenService.js'; import { createApiTokenService } from '../api-tokens/createApiTokenService.js';
import type User from '../../types/user.js'; import type User from '../../types/user.js';
import {
beforeAll,
expect,
test,
beforeEach,
afterEach,
afterAll,
} from 'vitest';
let stores: IUnleashStores; let stores: IUnleashStores;
let db: ITestDb; let db: ITestDb;
@ -672,7 +679,7 @@ describe('Managing Project access', () => {
[secondUser.id], [secondUser.id],
projectAuditUser, projectAuditUser,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new InvalidOperationError( new InvalidOperationError(
'User tried to grant role they did not have access to', 'User tried to grant role they did not have access to',
), ),
@ -746,7 +753,7 @@ describe('Managing Project access', () => {
[secondUser.id], [secondUser.id],
projectAuditUser, projectAuditUser,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new InvalidOperationError( new InvalidOperationError(
'User tried to grant role they did not have access to', 'User tried to grant role they did not have access to',
), ),
@ -868,7 +875,7 @@ describe('Managing Project access', () => {
[customRoleUpdateEnvironments.id], [customRoleUpdateEnvironments.id],
auditProjectUser, auditProjectUser,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new InvalidOperationError( new InvalidOperationError(
'User tried to assign a role they did not have access to', 'User tried to assign a role they did not have access to',
), ),
@ -885,7 +892,7 @@ describe('Managing Project access', () => {
[customRoleUpdateEnvironments.id], [customRoleUpdateEnvironments.id],
auditProjectUser, auditProjectUser,
), ),
).rejects.toThrow( ).rejects.errorWithMessage(
new InvalidOperationError( new InvalidOperationError(
'User tried to assign a role they did not have access to', 'User tried to assign a role they did not have access to',
), ),
@ -2641,12 +2648,12 @@ describe('create project with environments', () => {
}); });
test("envs that don't exist cause errors", async () => { test("envs that don't exist cause errors", async () => {
await expect(createProjectWithEnvs(['fake-project'])).rejects.toThrow( await expect(
BadDataError, createProjectWithEnvs(['fake-project']),
); ).rejects.toThrowError(BadDataError);
await expect(createProjectWithEnvs(['fake-project'])).rejects.toThrow( await expect(
/'fake-project'/, createProjectWithEnvs(['fake-project']),
); ).rejects.toThrowError(/'fake-project'/);
}); });
}); });

View File

@ -8,7 +8,7 @@ import {
} from '../../types/index.js'; } from '../../types/index.js';
import { createFakeProjectService } from './createProjectService.js'; import { createFakeProjectService } from './createProjectService.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
describe('enterprise extension: enable change requests', () => { describe('enterprise extension: enable change requests', () => {
const createService = (mode: 'oss' | 'enterprise' = 'enterprise') => { const createService = (mode: 'oss' | 'enterprise' = 'enterprise') => {
@ -75,7 +75,7 @@ describe('enterprise extension: enable change requests', () => {
test("it does not call the change request enablement function if we're not enterprise", async () => { test("it does not call the change request enablement function if we're not enterprise", async () => {
const { service } = createService('oss'); const { service } = createService('oss');
const fn = jest.fn() as () => Promise< const fn = vi.fn() as () => Promise<
{ name: string; requiredApprovals: number }[] | undefined { name: string; requiredApprovals: number }[] | undefined
>; >;
@ -334,7 +334,7 @@ describe('enterprise extension: enable change requests', () => {
const { service } = createService(); const { service } = createService();
const projectId = 'fake-project-id'; const projectId = 'fake-project-id';
expect( await expect(
service.createProject( service.createProject(
{ {
id: projectId, id: projectId,
@ -357,7 +357,7 @@ describe('enterprise extension: enable change requests', () => {
const { service } = createService('oss'); const { service } = createService('oss');
const projectId = 'fake-project-id'; const projectId = 'fake-project-id';
expect( await expect(
service.createProject( service.createProject(
{ {
id: projectId, id: projectId,

View File

@ -8,7 +8,7 @@ import type EventService from '../events/event-service.js';
import { SCHEDULER_JOB_TIME } from '../../metric-events.js'; import { SCHEDULER_JOB_TIME } from '../../metric-events.js';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { TEST_AUDIT_USER } from '../../types/index.js'; import { TEST_AUDIT_USER } from '../../types/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
function ms(timeMs) { function ms(timeMs) {
return new Promise((resolve) => setTimeout(resolve, timeMs)); return new Promise((resolve) => setTimeout(resolve, timeMs));
@ -72,7 +72,7 @@ test('Schedules job immediately', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const NO_JITTER = 0; const NO_JITTER = 0;
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 10, 'test-id', NO_JITTER); await schedulerService.schedule(job, 10, 'test-id', NO_JITTER);
@ -84,7 +84,7 @@ test('Does not schedule job immediately when paused', async () => {
const { schedulerService, maintenanceService } = const { schedulerService, maintenanceService } =
createSchedulerTestService(); createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await toggleMaintenanceMode(maintenanceService, true); await toggleMaintenanceMode(maintenanceService, true);
await schedulerService.schedule(job, 10, 'test-id-2'); await schedulerService.schedule(job, 10, 'test-id-2');
@ -96,7 +96,7 @@ test('Does not schedule job immediately when paused', async () => {
test('Can schedule a single regular job', async () => { test('Can schedule a single regular job', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 50, 'test-id-3'); await schedulerService.schedule(job, 50, 'test-id-3');
await ms(75); await ms(75);
@ -109,7 +109,7 @@ test('Scheduled job ignored in a paused mode', async () => {
const { schedulerService, maintenanceService } = const { schedulerService, maintenanceService } =
createSchedulerTestService(); createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await toggleMaintenanceMode(maintenanceService, true); await toggleMaintenanceMode(maintenanceService, true);
await schedulerService.schedule(job, 50, 'test-id-4'); await schedulerService.schedule(job, 50, 'test-id-4');
@ -123,7 +123,7 @@ test('Can resume paused job', async () => {
const { schedulerService, maintenanceService } = const { schedulerService, maintenanceService } =
createSchedulerTestService(); createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await toggleMaintenanceMode(maintenanceService, true); await toggleMaintenanceMode(maintenanceService, true);
await schedulerService.schedule(job, 50, 'test-id-5'); await schedulerService.schedule(job, 50, 'test-id-5');
@ -137,8 +137,8 @@ test('Can resume paused job', async () => {
test('Can schedule multiple jobs at the same interval', async () => { test('Can schedule multiple jobs at the same interval', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const anotherJob = jest.fn() as () => Promise<void>; const anotherJob = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 50, 'test-id-6'); await schedulerService.schedule(job, 50, 'test-id-6');
await schedulerService.schedule(anotherJob, 50, 'test-id-7'); await schedulerService.schedule(anotherJob, 50, 'test-id-7');
@ -152,8 +152,8 @@ test('Can schedule multiple jobs at the same interval', async () => {
test('Can schedule multiple jobs at the different intervals', async () => { test('Can schedule multiple jobs at the different intervals', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const anotherJob = jest.fn() as () => Promise<void>; const anotherJob = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 100, 'test-id-8'); await schedulerService.schedule(job, 100, 'test-id-8');
await schedulerService.schedule(anotherJob, 200, 'test-id-9'); await schedulerService.schedule(anotherJob, 200, 'test-id-9');
@ -242,7 +242,7 @@ it('should emit scheduler job time event when scheduled function is run', async
test('Delays initial job execution by jitter duration', async () => { test('Delays initial job execution by jitter duration', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const jitterMs = 10; const jitterMs = 10;
await schedulerService.schedule(job, 10000, 'test-id', jitterMs); await schedulerService.schedule(job, 10000, 'test-id', jitterMs);
@ -256,7 +256,7 @@ test('Delays initial job execution by jitter duration', async () => {
test('Does not apply jitter if schedule interval is smaller than max jitter', async () => { test('Does not apply jitter if schedule interval is smaller than max jitter', async () => {
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
// default jitter 2s-30s // default jitter 2s-30s
await schedulerService.schedule(job, 1000, 'test-id'); await schedulerService.schedule(job, 1000, 'test-id');
@ -269,7 +269,7 @@ test('Does not allow to run scheduled job when it is already pending', async ()
const { schedulerService } = createSchedulerTestService(); const { schedulerService } = createSchedulerTestService();
const NO_JITTER = 0; const NO_JITTER = 0;
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const slowJob = async () => { const slowJob = async () => {
job(); job();
await ms(25); await ms(25);

View File

@ -28,7 +28,7 @@ import type {
} from '../../openapi/index.js'; } from '../../openapi/index.js';
import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../util/index.js'; import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../util/index.js';
import { DEFAULT_PROJECT, TEST_AUDIT_USER } from '../../types/index.js'; import { DEFAULT_PROJECT, TEST_AUDIT_USER } from '../../types/index.js';
import { beforeAll, afterAll, afterEach, test, describe, expect } from 'vitest';
let db: ITestDb; let db: ITestDb;
let app: IUnleashTest; let app: IUnleashTest;
@ -55,11 +55,9 @@ const fetchFeatureStrategies = (featureName: string) =>
.expect(200) .expect(200)
.then((res) => res.body); .then((res) => res.body);
const fetchClientFeatures = (): Promise<IFeatureToggleClient[]> => { const fetchClientFeatures = async (): Promise<IFeatureToggleClient[]> => {
return app.request const res = await app.request.get(FEATURES_CLIENT_BASE_PATH).expect(200);
.get(FEATURES_CLIENT_BASE_PATH) return res.body.features;
.expect(200)
.then((res) => res.body.features);
}; };
const createSegment = (postData: UpsertSegmentSchema): Promise<ISegment> => { const createSegment = (postData: UpsertSegmentSchema): Promise<ISegment> => {
@ -235,7 +233,7 @@ test('should validate segment constraint values limit', async () => {
await expect( await expect(
createSegment({ name: randomId(), constraints }), createSegment({ name: randomId(), constraints }),
).rejects.toThrow( ).rejects.toThrowError(
`Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`, `Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`,
); );
}); });
@ -256,7 +254,7 @@ test('should validate segment constraint values limit with multiple constraints'
await expect( await expect(
createSegment({ name: randomId(), constraints }), createSegment({ name: randomId(), constraints }),
).rejects.toThrow( ).rejects.toThrowError(
`Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`, `Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`,
); );
}); });
@ -514,7 +512,7 @@ describe('project-specific segments', () => {
...segment, ...segment,
project: project2, project: project2,
}), }),
).rejects.toThrow( ).rejects.toThrowError(
`Invalid project. Segment is being used by strategies in other projects: ${project1}`, `Invalid project. Segment is being used by strategies in other projects: ${project1}`,
); );
}); });
@ -543,12 +541,12 @@ describe('project-specific segments', () => {
[strategy], [strategy],
project1, project1,
); );
await expect(() => await expect(
updateSegment(segment.id, { updateSegment(segment.id, {
...segment, ...segment,
project: '', project: '',
}), }),
).resolves; ).resolves.toBeUndefined();
}); });
test(`can't set a specific segment project when being used by multiple projects (global)`, async () => { test(`can't set a specific segment project when being used by multiple projects (global)`, async () => {
@ -589,12 +587,12 @@ describe('project-specific segments', () => {
[strategy2], [strategy2],
project2, project2,
); );
await expect(() => await expect(
updateSegment(segment.id, { updateSegment(segment.id, {
...segment, ...segment,
project: project1, project: project1,
}), }),
).rejects.toThrow( ).rejects.toThrowError(
`Invalid project. Segment is being used by strategies in other projects: ${project1}, ${project2}`, `Invalid project. Segment is being used by strategies in other projects: ${project1}, ${project2}`,
); );
}); });

View File

@ -9,7 +9,7 @@ import apiTokenMiddleware, {
} from './api-token-middleware.js'; } from './api-token-middleware.js';
import type { ApiTokenService } from '../services/index.js'; import type { ApiTokenService } from '../services/index.js';
import type { IUnleashConfig } from '../types/index.js'; import type { IUnleashConfig } from '../types/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let config: IUnleashConfig; let config: IUnleashConfig;
@ -24,15 +24,15 @@ beforeEach(() => {
test('should not do anything if request does not contain a authorization', async () => { test('should not do anything if request does not contain a authorization', async () => {
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn(), getUserForToken: vi.fn(),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn(), header: vi.fn(),
}; };
await func(req, undefined, cb); await func(req, undefined, cb);
@ -43,15 +43,15 @@ test('should not do anything if request does not contain a authorization', async
test('should not add user if unknown token', async () => { test('should not add user if unknown token', async () => {
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn(), getUserForToken: vi.fn(),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-token'), header: vi.fn().mockReturnValue('some-token'),
user: undefined, user: undefined,
}; };
@ -64,15 +64,15 @@ test('should not add user if unknown token', async () => {
test('should not make database query when provided PAT format', async () => { test('should not make database query when provided PAT format', async () => {
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn(), getUserForToken: vi.fn(),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('user:asdkjsdhg3'), header: vi.fn().mockReturnValue('user:asdkjsdhg3'),
user: undefined, user: undefined,
}; };
@ -94,15 +94,15 @@ test('should add user if known token', async () => {
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: vi.fn().mockReturnValue(apiUser),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-known-token'), header: vi.fn().mockReturnValue('some-known-token'),
user: undefined, user: undefined,
path: '/api/client', path: '/api/client',
}; };
@ -127,11 +127,11 @@ test('should not add user if not /api/client', async () => {
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: vi.fn().mockReturnValue(apiUser),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const res = { const res = {
status: (code: unknown) => ({ status: (code: unknown) => ({
@ -143,7 +143,7 @@ test('should not add user if not /api/client', async () => {
}; };
const req = { const req = {
header: jest.fn().mockReturnValue('some-known-token'), header: vi.fn().mockReturnValue('some-known-token'),
user: undefined, user: undefined,
path: '/api/admin', path: '/api/admin',
}; };
@ -165,7 +165,7 @@ test('should not add user if disabled', async () => {
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: vi.fn().mockReturnValue(apiUser),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const disabledConfig = createTestConfig({ const disabledConfig = createTestConfig({
@ -178,14 +178,14 @@ test('should not add user if disabled', async () => {
const func = apiTokenMiddleware(disabledConfig, { apiTokenService }); const func = apiTokenMiddleware(disabledConfig, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-known-token'), header: vi.fn().mockReturnValue('some-known-token'),
user: undefined, user: undefined,
}; };
const send = jest.fn(); const send = vi.fn();
const res = { const res = {
status: () => { status: () => {
return { return {
@ -211,10 +211,10 @@ test('should call next if apiTokenService throws', async () => {
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-token'), header: vi.fn().mockReturnValue('some-token'),
user: undefined, user: undefined,
}; };
@ -225,7 +225,7 @@ test('should call next if apiTokenService throws', async () => {
}); });
test('should call next if apiTokenService throws x2', async () => { test('should call next if apiTokenService throws x2', async () => {
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
const apiTokenService = { const apiTokenService = {
getUserForToken: () => { getUserForToken: () => {
throw new Error('hi there, i am stupid'); throw new Error('hi there, i am stupid');
@ -234,10 +234,10 @@ test('should call next if apiTokenService throws x2', async () => {
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-token'), header: vi.fn().mockReturnValue('some-token'),
user: undefined, user: undefined,
}; };
@ -256,15 +256,15 @@ test('should add user if client token and /edge/metrics', async () => {
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: vi.fn().mockReturnValue(apiUser),
} as unknown as ApiTokenService; } as unknown as ApiTokenService;
const func = apiTokenMiddleware(config, { apiTokenService }); const func = apiTokenMiddleware(config, { apiTokenService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('some-known-token'), header: vi.fn().mockReturnValue('some-known-token'),
user: undefined, user: undefined,
path: '/edge/metrics', path: '/edge/metrics',
method: 'POST', method: 'POST',

View File

@ -3,14 +3,14 @@ import type { IUnleashConfig } from '../types/index.js';
import { createTestConfig } from '../../test/config/test-config.js'; import { createTestConfig } from '../../test/config/test-config.js';
import getLogger from '../../test/fixtures/no-logger.js'; import getLogger from '../../test/fixtures/no-logger.js';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
const exampleSignalToken = 'signal_tokensecret'; const exampleSignalToken = 'signal_tokensecret';
describe('bearerTokenMiddleware', () => { describe('bearerTokenMiddleware', () => {
const req = { headers: {}, path: '' } as Request; const req = { headers: {}, path: '' } as Request;
const res = {} as Response; const res = {} as Response;
const next = jest.fn(); const next = vi.fn();
let config: IUnleashConfig; let config: IUnleashConfig;

View File

@ -1,6 +1,6 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import requireContentType from './content_type_checker.js'; import requireContentType from './content_type_checker.js';
import { jest } from '@jest/globals'; import { type Mock, vi } from 'vitest';
const mockRequest: (contentType: string) => Request = (contentType) => ({ const mockRequest: (contentType: string) => Request = (contentType) => ({
// @ts-ignore // @ts-ignore
@ -12,7 +12,7 @@ const mockRequest: (contentType: string) => Request = (contentType) => ({
}, },
}); });
const returns415: (t: jest.Mock) => Response = (t) => ({ const returns415: (t: Mock) => Response = (t) => ({
// @ts-ignore // @ts-ignore
status: (code) => { status: (code) => {
expect(415).toBe(code); expect(415).toBe(code);
@ -25,7 +25,7 @@ const returns415: (t: jest.Mock) => Response = (t) => ({
}, },
}); });
const expectNoCall: (t: jest.Mock) => Response = (t) => ({ const expectNoCall: (t: Mock) => Response = (t) => ({
// @ts-ignore // @ts-ignore
status: () => ({ status: () => ({
// @ts-ignore // @ts-ignore
@ -38,8 +38,8 @@ const expectNoCall: (t: jest.Mock) => Response = (t) => ({
test('Content-type middleware should by default only support application/json', () => { test('Content-type middleware should by default only support application/json', () => {
const middleware = requireContentType(); const middleware = requireContentType();
const t = jest.fn(); const t = vi.fn();
const fail = jest.fn(); const fail = vi.fn();
middleware(mockRequest('application/json'), expectNoCall(fail), t); middleware(mockRequest('application/json'), expectNoCall(fail), t);
middleware(mockRequest('text/plain'), returns415(t), fail); middleware(mockRequest('text/plain'), returns415(t), fail);
expect(t).toHaveBeenCalledTimes(2); expect(t).toHaveBeenCalledTimes(2);
@ -48,8 +48,8 @@ test('Content-type middleware should by default only support application/json',
test('Content-type middleware should by default only support application/json with charset', () => { test('Content-type middleware should by default only support application/json with charset', () => {
const middleware = requireContentType(); const middleware = requireContentType();
const t = jest.fn(); const t = vi.fn();
const fail = jest.fn(); const fail = vi.fn();
middleware( middleware(
mockRequest('application/json; charset=UTF-8'), mockRequest('application/json; charset=UTF-8'),
expectNoCall(fail), expectNoCall(fail),
@ -62,8 +62,8 @@ test('Content-type middleware should by default only support application/json wi
test('Content-type middleware should allow adding custom supported types', () => { test('Content-type middleware should allow adding custom supported types', () => {
const middleware = requireContentType('application/yaml'); const middleware = requireContentType('application/yaml');
const t = jest.fn(); const t = vi.fn();
const fail = jest.fn(); const fail = vi.fn();
middleware(mockRequest('application/yaml'), expectNoCall(fail), t); middleware(mockRequest('application/yaml'), expectNoCall(fail), t);
middleware(mockRequest('text/html'), returns415(t), fail); middleware(mockRequest('text/html'), returns415(t), fail);
middleware(mockRequest('text/plain'), returns415(t), fail); middleware(mockRequest('text/plain'), returns415(t), fail);
@ -73,8 +73,8 @@ test('Content-type middleware should allow adding custom supported types', () =>
test('adding custom supported types no longer supports default type', () => { test('adding custom supported types no longer supports default type', () => {
const middleware = requireContentType('application/yaml'); const middleware = requireContentType('application/yaml');
const t = jest.fn(); const t = vi.fn();
const fail = jest.fn(); const fail = vi.fn();
middleware(mockRequest('application/json'), returns415(t), fail); middleware(mockRequest('application/json'), returns415(t), fail);
expect(t).toHaveBeenCalledTimes(1); expect(t).toHaveBeenCalledTimes(1);
expect(fail).toHaveBeenCalledTimes(0); expect(fail).toHaveBeenCalledTimes(0);
@ -86,8 +86,8 @@ test('Should be able to add multiple content-types supported', () => {
'application/yaml', 'application/yaml',
'form/multipart', 'form/multipart',
); );
const fail = jest.fn(); const fail = vi.fn();
const succeed = jest.fn(); const succeed = vi.fn();
middleware(mockRequest('application/json'), expectNoCall(fail), succeed); middleware(mockRequest('application/json'), expectNoCall(fail), succeed);
middleware(mockRequest('application/yaml'), expectNoCall(fail), succeed); middleware(mockRequest('application/yaml'), expectNoCall(fail), succeed);
middleware(mockRequest('form/multipart'), expectNoCall(fail), succeed); middleware(mockRequest('form/multipart'), expectNoCall(fail), succeed);

View File

@ -4,7 +4,7 @@ import { createTestConfig } from '../../test/config/test-config.js';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { REQUEST_ORIGIN } from '../metric-events.js'; import { REQUEST_ORIGIN } from '../metric-events.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
const TEST_UNLEASH_TOKEN = 'TEST_UNLEASH_TOKEN'; const TEST_UNLEASH_TOKEN = 'TEST_UNLEASH_TOKEN';
const TEST_USER_AGENT = 'TEST_USER_AGENT'; const TEST_USER_AGENT = 'TEST_USER_AGENT';
@ -12,17 +12,17 @@ const TEST_USER_AGENT = 'TEST_USER_AGENT';
describe('originMiddleware', () => { describe('originMiddleware', () => {
const req = { headers: {}, path: '' } as Request; const req = { headers: {}, path: '' } as Request;
const res = {} as Response; const res = {} as Response;
const next = jest.fn(); const next = vi.fn();
const loggerMock = { const loggerMock = {
debug: jest.fn(), debug: vi.fn(),
info: jest.fn(), info: vi.fn(),
warn: jest.fn(), warn: vi.fn(),
error: jest.fn(), error: vi.fn(),
fatal: jest.fn(), fatal: vi.fn(),
}; };
const getLogger = jest.fn(() => loggerMock); const getLogger = vi.fn(() => loggerMock);
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
eventBus.emit = jest.fn() as () => boolean; eventBus.emit = vi.fn() as () => boolean;
let config: IUnleashConfig; let config: IUnleashConfig;

View File

@ -4,7 +4,7 @@ import User from '../types/user.js';
import NotFoundError from '../error/notfound-error.js'; import NotFoundError from '../error/notfound-error.js';
import type { AccountService } from '../services/account-service.js'; import type { AccountService } from '../services/account-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let config: any; let config: any;
@ -12,7 +12,7 @@ beforeEach(() => {
config = { config = {
getLogger, getLogger,
flagResolver: { flagResolver: {
isEnabled: jest.fn().mockReturnValue(true), isEnabled: vi.fn().mockReturnValue(true),
}, },
}; };
}); });
@ -20,16 +20,16 @@ beforeEach(() => {
test('should not set user if unknown token', async () => { test('should not set user if unknown token', async () => {
// @ts-expect-error wrong type // @ts-expect-error wrong type
const accountService = { const accountService = {
getAccountByPersonalAccessToken: jest.fn(), getAccountByPersonalAccessToken: vi.fn(),
addPATSeen: jest.fn(), addPATSeen: vi.fn(),
} as AccountService; } as AccountService;
const func = patMiddleware(config, { accountService }); const func = patMiddleware(config, { accountService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('user:some-token'), header: vi.fn().mockReturnValue('user:some-token'),
user: undefined, user: undefined,
}; };
@ -43,15 +43,15 @@ test('should not set user if unknown token', async () => {
test('should not set user if token wrong format', async () => { test('should not set user if token wrong format', async () => {
// @ts-expect-error wrong type // @ts-expect-error wrong type
const accountService = { const accountService = {
getAccountByPersonalAccessToken: jest.fn(), getAccountByPersonalAccessToken: vi.fn(),
} as AccountService; } as AccountService;
const func = patMiddleware(config, { accountService }); const func = patMiddleware(config, { accountService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('token-not-starting-with-user'), header: vi.fn().mockReturnValue('token-not-starting-with-user'),
user: undefined, user: undefined,
}; };
@ -72,16 +72,16 @@ test('should add user if known token', async () => {
}); });
// @ts-expect-error wrong type // @ts-expect-error wrong type
const accountService = { const accountService = {
getAccountByPersonalAccessToken: jest.fn().mockReturnValue(apiUser), getAccountByPersonalAccessToken: vi.fn().mockReturnValue(apiUser),
addPATSeen: jest.fn(), addPATSeen: vi.fn(),
} as AccountService; } as AccountService;
const func = patMiddleware(config, { accountService }); const func = patMiddleware(config, { accountService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('user:some-known-token'), header: vi.fn().mockReturnValue('user:some-known-token'),
user: undefined, user: undefined,
path: '/api/client', path: '/api/client',
}; };
@ -104,10 +104,10 @@ test('should call next if accountService throws exception', async () => {
const func = patMiddleware(config, { accountService }); const func = patMiddleware(config, { accountService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('user:some-token'), header: vi.fn().mockReturnValue('user:some-token'),
user: undefined, user: undefined,
}; };
@ -121,8 +121,8 @@ test('Should not log at error level if user not found', async () => {
const fakeLogger = { const fakeLogger = {
debug: () => {}, debug: () => {},
info: () => {}, info: () => {},
warn: jest.fn(), warn: vi.fn(),
error: jest.fn(), error: vi.fn(),
fatal: console.error, fatal: console.error,
}; };
const conf = { const conf = {
@ -130,20 +130,20 @@ test('Should not log at error level if user not found', async () => {
return fakeLogger; return fakeLogger;
}, },
flagResolver: { flagResolver: {
isEnabled: jest.fn().mockReturnValue(true), isEnabled: vi.fn().mockReturnValue(true),
}, },
}; };
// @ts-expect-error wrong type // @ts-expect-error wrong type
const accountService = { const accountService = {
getAccountByPersonalAccessToken: jest.fn().mockImplementation(() => { getAccountByPersonalAccessToken: vi.fn().mockImplementation(() => {
throw new NotFoundError('Could not find pat'); throw new NotFoundError('Could not find pat');
}), }),
} as AccountService; } as AccountService;
const mw = patMiddleware(conf, { accountService }); const mw = patMiddleware(conf, { accountService });
const cb = jest.fn(); const cb = vi.fn();
const req = { const req = {
header: jest.fn().mockReturnValue('user:some-token'), header: vi.fn().mockReturnValue('user:some-token'),
user: undefined, user: undefined,
}; };

View File

@ -10,7 +10,7 @@ import { ApiTokenType } from '../types/model.js';
import { type ISegmentStore, SYSTEM_USER_ID } from '../types/index.js'; import { type ISegmentStore, SYSTEM_USER_ID } from '../types/index.js';
import FakeSegmentStore from '../../test/fixtures/fake-segment-store.js'; import FakeSegmentStore from '../../test/fixtures/fake-segment-store.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let config: IUnleashConfig; let config: IUnleashConfig;
let featureToggleStore: IFeatureToggleStore; let featureToggleStore: IFeatureToggleStore;
@ -24,7 +24,7 @@ beforeEach(() => {
test('should add checkRbac to request', () => { test('should add checkRbac to request', () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -33,9 +33,9 @@ test('should add checkRbac to request', () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req = jest.fn(); const req = vi.fn();
func(req, undefined, cb); func(req, undefined, cb);
@ -47,7 +47,7 @@ test('should add checkRbac to request', () => {
test('should give api-user ADMIN permission', async () => { test('should give api-user ADMIN permission', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -56,7 +56,7 @@ test('should give api-user ADMIN permission', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new ApiUser({ user: new ApiUser({
tokenName: 'api', tokenName: 'api',
@ -79,7 +79,7 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
/// Will be -42 (ADMIN_USER.id) when we have the api-token-middleware run first /// Will be -42 (ADMIN_USER.id) when we have the api-token-middleware run first
test('Should give ADMIN api-user userid -1337 (SYSTEM_USER_ID)', async () => { test('Should give ADMIN api-user userid -1337 (SYSTEM_USER_ID)', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -88,7 +88,7 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new ApiUser({ user: new ApiUser({
tokenName: 'api', tokenName: 'api',
@ -107,7 +107,7 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
/// Will be -42 (ADMIN_USER.id) when we have the api-token-middleware run first /// Will be -42 (ADMIN_USER.id) when we have the api-token-middleware run first
test('Also when checking against permission NONE, userid should still be -1337', async () => { test('Also when checking against permission NONE, userid should still be -1337', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -116,7 +116,7 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new ApiUser({ user: new ApiUser({
tokenName: 'api', tokenName: 'api',
@ -136,7 +136,7 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
test('should not give api-user ADMIN permission', async () => { test('should not give api-user ADMIN permission', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -145,7 +145,7 @@ test('should not give api-user ADMIN permission', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new ApiUser({ user: new ApiUser({
tokenName: 'api', tokenName: 'api',
@ -166,9 +166,9 @@ test('should not give api-user ADMIN permission', async () => {
}); });
test('should not allow user to miss userId', async () => { test('should not allow user to miss userId', async () => {
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -177,7 +177,7 @@ test('should not allow user to miss userId', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: { user: {
username: 'user', username: 'user',
@ -192,9 +192,9 @@ test('should not allow user to miss userId', async () => {
}); });
test('should return false for missing user', async () => { test('should return false for missing user', async () => {
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -203,7 +203,7 @@ test('should return false for missing user', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = {}; const req: any = {};
func(req, undefined, cb); func(req, undefined, cb);
@ -216,7 +216,7 @@ test('should return false for missing user', async () => {
test('should verify permission for root resource', async () => { test('should verify permission for root resource', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -225,7 +225,7 @@ test('should verify permission for root resource', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ user: new User({
username: 'user', username: 'user',
@ -249,7 +249,7 @@ test('should verify permission for root resource', async () => {
test('should lookup projectId from params', async () => { test('should lookup projectId from params', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -258,7 +258,7 @@ test('should lookup projectId from params', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ user: new User({
username: 'user', username: 'user',
@ -286,11 +286,10 @@ test('should lookup projectId from feature flag', async () => {
const featureName = 'some-feature-flag'; const featureName = 'some-feature-flag';
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
// @ts-expect-error unknown fn type featureToggleStore.getProjectId = vi.fn().mockReturnValue(projectId);
featureToggleStore.getProjectId = jest.fn().mockReturnValue(projectId);
const func = rbacMiddleware( const func = rbacMiddleware(
config, config,
@ -298,7 +297,7 @@ test('should lookup projectId from feature flag', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ user: new User({
username: 'user', username: 'user',
@ -326,7 +325,7 @@ test('should lookup projectId from data', async () => {
const featureName = 'some-feature-flag'; const featureName = 'some-feature-flag';
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -335,7 +334,7 @@ test('should lookup projectId from data', async () => {
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ user: new User({
username: 'user', username: 'user',
@ -364,17 +363,16 @@ test('Does not double check permission if not changing project when updating fla
const oldProjectId = 'some-project-34'; const oldProjectId = 'some-project-34';
const featureName = 'some-feature-flag'; const featureName = 'some-feature-flag';
const accessService = { const accessService = {
hasPermission: jest.fn().mockReturnValue(true), hasPermission: vi.fn().mockReturnValue(true),
} as PermissionChecker; } as PermissionChecker;
// @ts-expect-error unknown fn type featureToggleStore.getProjectId = vi.fn().mockReturnValue(oldProjectId);
featureToggleStore.getProjectId = jest.fn().mockReturnValue(oldProjectId);
const func = rbacMiddleware( const func = rbacMiddleware(
config, config,
{ featureToggleStore, segmentStore }, { featureToggleStore, segmentStore },
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ username: 'user', id: 1 }), user: new User({ username: 'user', id: 1 }),
params: { featureName }, params: { featureName },
@ -394,7 +392,7 @@ test('Does not double check permission if not changing project when updating fla
test('CREATE_TAG_TYPE does not need projectId', async () => { test('CREATE_TAG_TYPE does not need projectId', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn().mockReturnValue(true), hasPermission: vi.fn().mockReturnValue(true),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -402,7 +400,7 @@ test('CREATE_TAG_TYPE does not need projectId', async () => {
{ featureToggleStore, segmentStore }, { featureToggleStore, segmentStore },
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ username: 'user', id: 1 }), user: new User({ username: 'user', id: 1 }),
params: {}, params: {},
@ -422,7 +420,7 @@ test('CREATE_TAG_TYPE does not need projectId', async () => {
test('UPDATE_TAG_TYPE does not need projectId', async () => { test('UPDATE_TAG_TYPE does not need projectId', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn().mockReturnValue(true), hasPermission: vi.fn().mockReturnValue(true),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -430,7 +428,7 @@ test('UPDATE_TAG_TYPE does not need projectId', async () => {
{ featureToggleStore, segmentStore }, { featureToggleStore, segmentStore },
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ username: 'user', id: 1 }), user: new User({ username: 'user', id: 1 }),
params: {}, params: {},
@ -450,7 +448,7 @@ test('UPDATE_TAG_TYPE does not need projectId', async () => {
test('DELETE_TAG_TYPE does not need projectId', async () => { test('DELETE_TAG_TYPE does not need projectId', async () => {
const accessService = { const accessService = {
hasPermission: jest.fn().mockReturnValue(true), hasPermission: vi.fn().mockReturnValue(true),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -458,7 +456,7 @@ test('DELETE_TAG_TYPE does not need projectId', async () => {
{ featureToggleStore, segmentStore }, { featureToggleStore, segmentStore },
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ username: 'user', id: 1 }), user: new User({ username: 'user', id: 1 }),
params: {}, params: {},
@ -480,7 +478,7 @@ test('should not expect featureName for UPDATE_FEATURE when projectId specified'
const projectId = 'some-project-33'; const projectId = 'some-project-33';
const accessService = { const accessService = {
hasPermission: jest.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
const func = rbacMiddleware( const func = rbacMiddleware(
@ -489,7 +487,7 @@ test('should not expect featureName for UPDATE_FEATURE when projectId specified'
accessService, accessService,
); );
const cb = jest.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new User({ user: new User({
username: 'user', username: 'user',

View File

@ -3,7 +3,7 @@ import {
storeRequestedRoute, storeRequestedRoute,
} from './response-time-metrics.js'; } from './response-time-metrics.js';
import { REQUEST_TIME } from '../metric-events.js'; import { REQUEST_TIME } from '../metric-events.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import type { IFlagResolver } from '../server-impl.js'; import type { IFlagResolver } from '../server-impl.js';
import EventEmitter from 'events'; import EventEmitter from 'events';
@ -20,10 +20,10 @@ const isDefined = async (timeInfo: any, limit = 10) => {
}; };
const flagResolver = { const flagResolver = {
isEnabled: jest.fn(), isEnabled: vi.fn(),
getAll: jest.fn(), getAll: vi.fn(),
getVariant: jest.fn(), getVariant: vi.fn(),
getStaticContext: jest.fn(), getStaticContext: vi.fn(),
} as IFlagResolver; } as IFlagResolver;
// Make sure it's always cleaned up // Make sure it's always cleaned up
@ -32,7 +32,7 @@ beforeEach(() => {
res = { res = {
statusCode: 200, statusCode: 200,
locals: {}, // res will always have locals (according to express RequestHandler type) locals: {}, // res will always have locals (according to express RequestHandler type)
once: jest.fn((event: string, callback: () => void) => { once: vi.fn((event: string, callback: () => void) => {
if (event === 'finish') { if (event === 'finish') {
callback(); callback();
} }
@ -42,7 +42,7 @@ beforeEach(() => {
describe('responseTimeMetrics new behavior', () => { describe('responseTimeMetrics new behavior', () => {
const instanceStatsService = { const instanceStatsService = {
getAppCountSnapshot: jest.fn() as () => number | undefined, getAppCountSnapshot: vi.fn() as () => number | undefined,
}; };
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();

View File

@ -134,6 +134,11 @@ describe.each(metaRules)('OpenAPI schemas $name', (rule) => {
expect(validateMetaSchema.errors).toBeNull(); expect(validateMetaSchema.errors).toBeNull();
} }
}); });
} else {
// Added, because vitest requires tests for all exceptions.
it(`${schemaName}`, () => {
expect(true).toBe(true);
});
} }
}); });
}); });

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`updateEnvironmentSchema 1`] = ` exports[`updateEnvironmentSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`apiTokenSchema empty 1`] = ` exports[`apiTokenSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`changePasswordSchema empty 1`] = ` exports[`changePasswordSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`clientApplicationSchema no fields 1`] = ` exports[`clientApplicationSchema no fields 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`clientFeaturesSchema no fields 1`] = ` exports[`clientFeaturesSchema no fields 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`clientMetricsSchema should fail when required field is missing 1`] = ` exports[`clientMetricsSchema should fail when required field is missing 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`constraintSchema invalid operator name 1`] = ` exports[`constraintSchema invalid operator name 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`contextFieldSchema empty 1`] = ` exports[`contextFieldSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`deprecatedProjectOverviewSchema 1`] = ` exports[`deprecatedProjectOverviewSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`emailSchema 1`] = ` exports[`emailSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`featureEnvironmentSchema empty 1`] = ` exports[`featureEnvironmentSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`featureSchema constraints 1`] = ` exports[`featureSchema constraints 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`featureTypeCountSchema 1`] = ` exports[`featureTypeCountSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`featureTypeSchema empty 1`] = ` exports[`featureTypeSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`meSchema empty 1`] = ` exports[`meSchema empty 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`projectOverviewSchema 1`] = ` exports[`projectOverviewSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`roleSchema 1`] = ` exports[`roleSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`segmentStrategiesSchema 1`] = ` exports[`segmentStrategiesSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`updateEnvironmentSchema 1`] = `undefined`; exports[`updateEnvironmentSchema 1`] = `undefined`;

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`setStrategySortOrderSchema missing id 1`] = ` exports[`setStrategySortOrderSchema missing id 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sortOrderSchema invalid value type 1`] = ` exports[`sortOrderSchema invalid value type 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`strategySchema 1`] = ` exports[`strategySchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`tokenUserSchema 1`] = ` exports[`tokenUserSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`updateFeatureStrategySegmentsSchema schema 1`] = ` exports[`updateFeatureStrategySegmentsSchema schema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`upsertSegmentSchema 1`] = ` exports[`upsertSegmentSchema 1`] = `
{ {

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`validatePasswordSchema empty 1`] = ` exports[`validatePasswordSchema empty 1`] = `
{ {

View File

@ -6,6 +6,7 @@ import {
} from '../../../lib/openapi/spec/playground-request-schema.js'; } from '../../../lib/openapi/spec/playground-request-schema.js';
import { validateSchema } from '../validate.js'; import { validateSchema } from '../validate.js';
import { generate as generateContext } from './sdk-context-schema.test.js'; import { generate as generateContext } from './sdk-context-schema.test.js';
import { test } from '@fast-check/vitest';
export const generate = (): Arbitrary<PlaygroundRequestSchema> => export const generate = (): Arbitrary<PlaygroundRequestSchema> =>
fc.record({ fc.record({

View File

@ -6,7 +6,7 @@ import {
import { validateSchema } from '../validate.js'; import { validateSchema } from '../validate.js';
import { generate as generateInput } from './playground-request-schema.test.js'; import { generate as generateInput } from './playground-request-schema.test.js';
import { generate as generateFeature } from './playground-feature-schema.test.js'; import { generate as generateFeature } from './playground-feature-schema.test.js';
import { test } from '@fast-check/vitest';
const generate = (): Arbitrary<PlaygroundResponseSchema> => const generate = (): Arbitrary<PlaygroundResponseSchema> =>
fc.record({ fc.record({
input: generateInput(), input: generateInput(),
@ -15,7 +15,9 @@ const generate = (): Arbitrary<PlaygroundResponseSchema> =>
}), }),
}); });
test('playgroundResponseSchema', () => test(
'playgroundResponseSchema',
() =>
fc.assert( fc.assert(
fc.property( fc.property(
generate(), generate(),
@ -23,4 +25,6 @@ test('playgroundResponseSchema', () =>
validateSchema(playgroundResponseSchema.$id, data) === validateSchema(playgroundResponseSchema.$id, data) ===
undefined, undefined,
), ),
)); ),
{ timeout: 60000 },
);

View File

@ -5,7 +5,7 @@ import {
sdkContextSchema, sdkContextSchema,
} from './sdk-context-schema.js'; } from './sdk-context-schema.js';
import { commonISOTimestamp } from '../../../test/arbitraries.test.js'; import { commonISOTimestamp } from '../../../test/arbitraries.test.js';
import { test } from '@fast-check/vitest';
export const generate = (): Arbitrary<SdkContextSchema> => export const generate = (): Arbitrary<SdkContextSchema> =>
fc.record( fc.record(
{ {

View File

@ -7,7 +7,7 @@ import permissions from '../../../test/fixtures/permissions.js';
import { RoleName, RoleType } from '../../types/model.js'; import { RoleName, RoleType } from '../../types/model.js';
import type { IUnleashStores } from '../../types/index.js'; import type { IUnleashStores } from '../../types/index.js';
import type TestAgent from 'supertest/lib/agent.d.ts'; import type TestAgent from 'supertest/lib/agent.d.ts';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
describe('Public Signup API', () => { describe('Public Signup API', () => {
async function getSetup() { async function getSetup() {
@ -19,8 +19,8 @@ describe('Public Signup API', () => {
stores.accessStore = { stores.accessStore = {
...stores.accessStore, ...stores.accessStore,
addUserToRole: jest.fn() as () => Promise<void>, addUserToRole: vi.fn() as () => Promise<void>,
removeRolesOfTypeForUser: jest.fn() as () => Promise<void>, removeRolesOfTypeForUser: vi.fn() as () => Promise<void>,
}; };
const services = createServices(stores, config); const services = createServices(stores, config);

View File

@ -4,7 +4,7 @@ import createStores from '../../../test/fixtures/store.js';
import permissions from '../../../test/fixtures/permissions.js'; import permissions from '../../../test/fixtures/permissions.js';
import getApp from '../../app.js'; import getApp from '../../app.js';
import { createServices } from '../../services/index.js'; import { createServices } from '../../services/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
async function getSetup() { async function getSetup() {
const randomBase = `/random${Math.round(Math.random() * 1000)}`; const randomBase = `/random${Math.round(Math.random() * 1000)}`;
@ -113,14 +113,14 @@ test('validate format when updating strategy', async () => {
}); });
test('editable=false will stop delete request', async () => { test('editable=false will stop delete request', async () => {
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
const { request, base } = await getSetup(); const { request, base } = await getSetup();
const name = 'default'; const name = 'default';
return request.delete(`${base}/api/admin/strategies/${name}`).expect(500); return request.delete(`${base}/api/admin/strategies/${name}`).expect(500);
}); });
test('editable=false will stop edit request', async () => { test('editable=false will stop edit request', async () => {
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());
const { request, base } = await getSetup(); const { request, base } = await getSetup();
const name = 'default'; const name = 'default';
return request return request

View File

@ -8,7 +8,7 @@ import SessionService from '../services/session-service.js';
import FakeSessionStore from '../../test/fixtures/fake-session-store.js'; import FakeSessionStore from '../../test/fixtures/fake-session-store.js';
import noLogger from '../../test/fixtures/no-logger.js'; import noLogger from '../../test/fixtures/no-logger.js';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
test('should redirect to "/" after logout', async () => { test('should redirect to "/" after logout', async () => {
const baseUriPath = ''; const baseUriPath = '';
@ -146,7 +146,7 @@ test('should clear "unleash-session" cookie even when disabled clear site data',
test('should call destroy on session', async () => { test('should call destroy on session', async () => {
const baseUriPath = ''; const baseUriPath = '';
const fakeSession = { const fakeSession = {
destroy: jest.fn(), destroy: vi.fn(),
}; };
const app = express(); const app = express();
const config = createTestConfig({ server: { baseUriPath } }); const config = createTestConfig({ server: { baseUriPath } });
@ -171,7 +171,7 @@ test('should call destroy on session', async () => {
test('should handle req.logout with callback function', async () => { test('should handle req.logout with callback function', async () => {
// passport >=0.6.0 // passport >=0.6.0
const baseUriPath = ''; const baseUriPath = '';
const logoutFunction = jest.fn((cb: (err?: any) => void) => cb()); const logoutFunction = vi.fn((cb: (err?: any) => void) => cb());
const app = express(); const app = express();
const config = createTestConfig({ server: { baseUriPath } }); const config = createTestConfig({ server: { baseUriPath } });
app.use((req: IAuthRequest, res, next) => { app.use((req: IAuthRequest, res, next) => {
@ -196,7 +196,7 @@ test('should handle req.logout with callback function', async () => {
test('should handle req.logout without callback function', async () => { test('should handle req.logout without callback function', async () => {
// passport <0.6.0 // passport <0.6.0
const baseUriPath = ''; const baseUriPath = '';
const logoutFunction = jest.fn(); const logoutFunction = vi.fn();
const app = express(); const app = express();
const config = createTestConfig({ server: { baseUriPath } }); const config = createTestConfig({ server: { baseUriPath } });
app.use((req: IAuthRequest, res, next) => { app.use((req: IAuthRequest, res, next) => {
@ -220,7 +220,7 @@ test('should handle req.logout without callback function', async () => {
test('should redirect to alternative logoutUrl', async () => { test('should redirect to alternative logoutUrl', async () => {
const fakeSession = { const fakeSession = {
destroy: jest.fn(), destroy: vi.fn(),
logoutUrl: '/some-other-path', logoutUrl: '/some-other-path',
}; };
const app = express(); const app = express();
@ -248,7 +248,7 @@ test('Should destroy sessions for user', async () => {
const app = express(); const app = express();
const config = createTestConfig(); const config = createTestConfig();
const fakeSession = { const fakeSession = {
destroy: jest.fn(), destroy: vi.fn(),
user: { user: {
id: 1, id: 1,
}, },

View File

@ -7,7 +7,7 @@ import permissions from '../../test/fixtures/permissions.js';
import { RoleName, RoleType } from '../types/model.js'; import { RoleName, RoleType } from '../types/model.js';
import type { IUnleashStores } from '../types/index.js'; import type { IUnleashStores } from '../types/index.js';
import type TestAgent from 'supertest/lib/agent.d.ts'; import type TestAgent from 'supertest/lib/agent.d.ts';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
describe('Public Signup API', () => { describe('Public Signup API', () => {
async function getSetup() { async function getSetup() {
@ -19,8 +19,8 @@ describe('Public Signup API', () => {
stores.accessStore = { stores.accessStore = {
...stores.accessStore, ...stores.accessStore,
addUserToRole: jest.fn() as () => Promise<void>, addUserToRole: vi.fn() as () => Promise<void>,
removeRolesOfTypeForUser: jest.fn() as () => Promise<void>, removeRolesOfTypeForUser: vi.fn() as () => Promise<void>,
getRolesForUserId: () => Promise.resolve([]), getRolesForUserId: () => Promise.resolve([]),
getRootRoleForUser: () => getRootRoleForUser: () =>
Promise.resolve({ Promise.resolve({

View File

@ -10,25 +10,25 @@ import {
start, start,
type UnleashFactoryMethods, type UnleashFactoryMethods,
} from './server-impl.js'; } from './server-impl.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
import type MetricsMonitor from './metrics.js'; import type MetricsMonitor from './metrics.js';
const mockFactories: () => UnleashFactoryMethods = () => ({ const mockFactories: () => UnleashFactoryMethods = () => ({
createDb: jest.fn<(config: IUnleashConfig) => Db>().mockReturnValue({ createDb: vi.fn<(config: IUnleashConfig) => Db>().mockReturnValue({
destroy: jest.fn(), destroy: vi.fn(),
} as unknown as Db), } as unknown as Db),
createStores: jest createStores: vi
.fn<(config: IUnleashConfig, db: Db) => IUnleashStores>() .fn<(config: IUnleashConfig, db: Db) => IUnleashStores>()
.mockReturnValue({ .mockReturnValue({
settingStore: { settingStore: {
get: jest.fn(), get: vi.fn(),
postgresVersion: jest.fn(), postgresVersion: vi.fn(),
}, },
eventStore: { eventStore: {
on: jest.fn(), on: vi.fn(),
}, },
} as unknown as IUnleashStores), } as unknown as IUnleashStores),
createServices: jest createServices: vi
.fn< .fn<
( (
stores: IUnleashStores, stores: IUnleashStores,
@ -38,24 +38,24 @@ const mockFactories: () => UnleashFactoryMethods = () => ({
>() >()
.mockReturnValue({ .mockReturnValue({
userService: { userService: {
initAdminUser: jest.fn(), initAdminUser: vi.fn(),
}, },
schedulerService: { schedulerService: {
schedule: jest.fn(), schedule: vi.fn(),
stop: jest.fn(), stop: vi.fn(),
}, },
addonService: { addonService: {
destroy: jest.fn(), destroy: vi.fn(),
}, },
openApiService: { openApiService: {
// returns a middleware // returns a middleware
validPath: jest.fn().mockReturnValue(() => {}), validPath: vi.fn().mockReturnValue(() => {}),
}, },
} as unknown as IUnleashServices), } as unknown as IUnleashServices),
createSessionDb: createSessionDb:
jest.fn<(config: IUnleashConfig, db: Db) => RequestHandler>(), vi.fn<(config: IUnleashConfig, db: Db) => RequestHandler>(),
createMetricsMonitor: jest.fn<() => MetricsMonitor>().mockReturnValue({ createMetricsMonitor: vi.fn<() => MetricsMonitor>().mockReturnValue({
startMonitoring: jest.fn(), startMonitoring: vi.fn(),
} as unknown as MetricsMonitor), } as unknown as MetricsMonitor),
}); });

View File

@ -27,6 +27,7 @@ import BadDataError from '../../lib/error/bad-data-error.js';
import { createFakeEventsService } from '../../lib/features/events/createEventsService.js'; import { createFakeEventsService } from '../../lib/features/events/createEventsService.js';
import { createFakeAccessReadModel } from '../features/access/createAccessReadModel.js'; import { createFakeAccessReadModel } from '../features/access/createAccessReadModel.js';
import { ROLE_CREATED } from '../events/index.js'; import { ROLE_CREATED } from '../events/index.js';
import { expect } from 'vitest';
function getSetup() { function getSetup() {
const config = createTestConfig({ const config = createTestConfig({
@ -56,10 +57,14 @@ test('should fail when name exists', async () => {
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
); );
expect(accessService.validateRole(existingRole)).rejects.toThrow( await expect(() =>
accessService.validateRole(existingRole),
).rejects.toThrowError(
expect.errorWithMessage(
new NameExistsError( new NameExistsError(
`There already exists a role with the name ${existingRole.name}`, `There already exists a role with the name ${existingRole.name}`,
), ),
),
); );
}); });
@ -110,7 +115,7 @@ test('should not accept empty names', async () => {
await expect( await expect(
accessService.validateRole(withWhitespaceName), accessService.validateRole(withWhitespaceName),
).rejects.toThrow('"name" is not allowed to be empty'); ).rejects.toThrowError('"name" is not allowed to be empty');
}); });
test('should trim leading and trailing whitespace from names', async () => { test('should trim leading and trailing whitespace from names', async () => {

View File

@ -14,7 +14,7 @@ import {
API_TOKEN_DELETED, API_TOKEN_DELETED,
API_TOKEN_UPDATED, API_TOKEN_UPDATED,
} from '../events/index.js'; } from '../events/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
test('Should init api token', async () => { test('Should init api token', async () => {
const token = { const token = {
@ -170,7 +170,7 @@ describe('API token getTokenWithCache', () => {
test('should return the token and perform only one db query', async () => { test('should return the token and perform only one db query', async () => {
const { apiTokenService, apiTokenStore } = setup(); const { apiTokenService, apiTokenStore } = setup();
const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get'); const apiTokenStoreGet = vi.spyOn(apiTokenStore, 'get');
// valid token not present in cache (could be inserted by another instance) // valid token not present in cache (could be inserted by another instance)
apiTokenStore.insert(token); apiTokenStore.insert(token);
@ -185,9 +185,9 @@ describe('API token getTokenWithCache', () => {
}); });
test('should query the db only once for invalid tokens', async () => { test('should query the db only once for invalid tokens', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
const { apiTokenService, apiTokenStore } = setup(); const { apiTokenService, apiTokenStore } = setup();
const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get'); const apiTokenStoreGet = vi.spyOn(apiTokenStore, 'get');
const invalidToken = 'invalid-token'; const invalidToken = 'invalid-token';
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
@ -198,7 +198,7 @@ describe('API token getTokenWithCache', () => {
expect(apiTokenStoreGet).toHaveBeenCalledTimes(1); expect(apiTokenStoreGet).toHaveBeenCalledTimes(1);
// after more than 5 minutes we should be able to query again // after more than 5 minutes we should be able to query again
jest.advanceTimersByTime(minutesToMilliseconds(6)); vi.advanceTimersByTime(minutesToMilliseconds(6));
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
expect( expect(
await apiTokenService.getTokenWithCache(invalidToken), await apiTokenService.getTokenWithCache(invalidToken),
@ -209,7 +209,7 @@ describe('API token getTokenWithCache', () => {
test('should not return the token if it has expired and shoud perform only one db query', async () => { test('should not return the token if it has expired and shoud perform only one db query', async () => {
const { apiTokenService, apiTokenStore } = setup(); const { apiTokenService, apiTokenStore } = setup();
const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get'); const apiTokenStoreGet = vi.spyOn(apiTokenStore, 'get');
// valid token not present in cache but expired // valid token not present in cache but expired
apiTokenStore.insert({ ...token, expiresAt: subDays(new Date(), 1) }); apiTokenStore.insert({ ...token, expiresAt: subDays(new Date(), 1) });
@ -234,7 +234,7 @@ test('normalizes api token type casing to lowercase', async () => {
sortOrder: 1, sortOrder: 1,
}); });
const apiTokenStoreInsert = jest.spyOn(apiTokenStore, 'insert'); const apiTokenStoreInsert = vi.spyOn(apiTokenStore, 'insert');
await apiTokenService.createApiTokenWithProjects( await apiTokenService.createApiTokenWithProjects(
{ {

View File

@ -1,7 +1,7 @@
import { EmailService, type TransportProvider } from './email-service.js'; import { EmailService, type TransportProvider } from './email-service.js';
import noLoggerProvider from '../../test/fixtures/no-logger.js'; import noLoggerProvider from '../../test/fixtures/no-logger.js';
import type { IUnleashConfig } from '../types/index.js'; import type { IUnleashConfig } from '../types/index.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
test('Can send reset email', async () => { test('Can send reset email', async () => {
const emailService = new EmailService({ const emailService = new EmailService({
@ -54,7 +54,7 @@ test('Can send welcome mail', async () => {
}); });
test('Can supply additional SMTP transport options', async () => { test('Can supply additional SMTP transport options', async () => {
const transport = jest.fn() as unknown as TransportProvider; const transport = vi.fn() as unknown as TransportProvider;
new EmailService( new EmailService(
{ {

View File

@ -6,7 +6,7 @@ import SettingService from './setting-service.js';
import type EventService from '../features/events/event-service.js'; import type EventService from '../features/events/event-service.js';
import MaintenanceService from '../features/maintenance/maintenance-service.js'; import MaintenanceService from '../features/maintenance/maintenance-service.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
function ms(timeMs: number) { function ms(timeMs: number) {
return new Promise((resolve) => setTimeout(resolve, timeMs)); return new Promise((resolve) => setTimeout(resolve, timeMs));
@ -51,7 +51,7 @@ beforeEach(() => {
}); });
test('Schedules job immediately', async () => { test('Schedules job immediately', async () => {
const job = jest.fn() as () => Promise<void> as () => Promise<void>; const job = vi.fn() as () => Promise<void> as () => Promise<void>;
await schedulerService.schedule(job, 10, 'test-id'); await schedulerService.schedule(job, 10, 'test-id');
expect(job).toHaveBeenCalledTimes(1); expect(job).toHaveBeenCalledTimes(1);
@ -59,7 +59,7 @@ test('Schedules job immediately', async () => {
}); });
test('Can schedule a single regular job', async () => { test('Can schedule a single regular job', async () => {
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 50, 'test-id-3'); await schedulerService.schedule(job, 50, 'test-id-3');
await ms(75); await ms(75);
@ -68,8 +68,8 @@ test('Can schedule a single regular job', async () => {
}); });
test('Can schedule multiple jobs at the same interval', async () => { test('Can schedule multiple jobs at the same interval', async () => {
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const anotherJob = jest.fn() as () => Promise<void>; const anotherJob = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 50, 'test-id-6'); await schedulerService.schedule(job, 50, 'test-id-6');
await schedulerService.schedule(anotherJob, 50, 'test-id-7'); await schedulerService.schedule(anotherJob, 50, 'test-id-7');
@ -81,8 +81,8 @@ test('Can schedule multiple jobs at the same interval', async () => {
}); });
test('Can schedule multiple jobs at the different intervals', async () => { test('Can schedule multiple jobs at the different intervals', async () => {
const job = jest.fn() as () => Promise<void>; const job = vi.fn() as () => Promise<void>;
const anotherJob = jest.fn() as () => Promise<void>; const anotherJob = vi.fn() as () => Promise<void>;
await schedulerService.schedule(job, 100, 'test-id-8'); await schedulerService.schedule(job, 100, 'test-id-8');
await schedulerService.schedule(anotherJob, 200, 'test-id-9'); await schedulerService.schedule(anotherJob, 200, 'test-id-9');

View File

@ -1,4 +1,5 @@
import { tagSchema } from './tag-schema.js'; import { tagSchema } from './tag-schema.js';
import { test, expect } from 'vitest';
test('should require url friendly type if defined', () => { test('should require url friendly type if defined', () => {
const tag = { const tag = {
@ -8,7 +9,7 @@ test('should require url friendly type if defined', () => {
const { error } = tagSchema.validate(tag); const { error } = tagSchema.validate(tag);
if (error === undefined) { if (error === undefined) {
fail('Did not receive an expected error'); expect.fail('Did not receive an expected error');
} }
expect(error.details[0].message).toEqual('"type" must be URL friendly'); expect(error.details[0].message).toEqual('"type" must be URL friendly');
}); });

View File

@ -15,8 +15,7 @@ import SettingService from './setting-service.js';
import FakeSettingStore from '../../test/fixtures/fake-setting-store.js'; import FakeSettingStore from '../../test/fixtures/fake-setting-store.js';
import { extractAuditInfoFromUser } from '../util/index.js'; import { extractAuditInfoFromUser } from '../util/index.js';
import { createFakeEventsService } from '../features/index.js'; import { createFakeEventsService } from '../features/index.js';
import { jest } from '@jest/globals'; import { vi, expect, test, describe, beforeEach } from 'vitest';
const config: IUnleashConfig = createTestConfig(); const config: IUnleashConfig = createTestConfig();
const systemUser = new User({ id: -1, username: 'system' }); const systemUser = new User({ id: -1, username: 'system' });
@ -76,11 +75,10 @@ describe('Default admin initialization', () => {
const CUSTOM_ADMIN_PASSWORD = 'custom-password'; const CUSTOM_ADMIN_PASSWORD = 'custom-password';
let userService: UserService; let userService: UserService;
const sendGettingStartedMailMock = const sendGettingStartedMailMock = vi.fn() as () => Promise<IEmailEnvelope>;
jest.fn() as () => Promise<IEmailEnvelope>;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
const userStore = new UserStoreMock(); const userStore = new UserStoreMock();
const accessService = new AccessServiceMock(); const accessService = new AccessServiceMock();
@ -91,7 +89,7 @@ describe('Default admin initialization', () => {
); );
const emailService = new EmailService(config); const emailService = new EmailService(config);
emailService.configured = jest.fn(() => true); emailService.configured = vi.fn(() => true);
emailService.sendGettingStartedMail = sendGettingStartedMailMock; emailService.sendGettingStartedMail = sendGettingStartedMailMock;
const sessionStore = new FakeSessionStore(); const sessionStore = new FakeSessionStore();
@ -187,7 +185,7 @@ describe('Default admin initialization', () => {
}); });
test('Should use the correct environment variables when initializing the default admin account', async () => { test('Should use the correct environment variables when initializing the default admin account', async () => {
jest.resetModules(); vi.resetModules();
process.env.UNLEASH_DEFAULT_ADMIN_USERNAME = CUSTOM_ADMIN_USERNAME; process.env.UNLEASH_DEFAULT_ADMIN_USERNAME = CUSTOM_ADMIN_USERNAME;
process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD = CUSTOM_ADMIN_PASSWORD; process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD = CUSTOM_ADMIN_PASSWORD;
@ -465,7 +463,7 @@ test('Should send password reset email if user exists', async () => {
}); });
const knownUser = service.createResetPasswordEmail('known@example.com'); const knownUser = service.createResetPasswordEmail('known@example.com');
expect(knownUser).resolves.toBeInstanceOf(URL); await expect(knownUser).resolves.toBeInstanceOf(URL);
}); });
test('Should throttle password reset email', async () => { test('Should throttle password reset email', async () => {
@ -511,17 +509,17 @@ test('Should throttle password reset email', async () => {
generateImageUrl: () => '', generateImageUrl: () => '',
}); });
jest.useFakeTimers(); vi.useFakeTimers();
const attempt1 = service.createResetPasswordEmail('known@example.com'); const attempt1 = service.createResetPasswordEmail('known@example.com');
await expect(attempt1).resolves.toBeInstanceOf(URL); await expect(attempt1).resolves.toBeInstanceOf(URL);
const attempt2 = service.createResetPasswordEmail('known@example.com'); const attempt2 = service.createResetPasswordEmail('known@example.com');
await expect(attempt2).rejects.toThrow( await expect(attempt2).rejects.toThrowError(
'You can only send one new reset password email per minute, per user. Please try again later.', 'You can only send one new reset password email per minute, per user. Please try again later.',
); );
jest.runAllTimers(); vi.runAllTimers();
const attempt3 = service.createResetPasswordEmail('known@example.com'); const attempt3 = service.createResetPasswordEmail('known@example.com');
await expect(attempt3).resolves.toBeInstanceOf(URL); await expect(attempt3).resolves.toBeInstanceOf(URL);

View File

@ -1,7 +1,6 @@
import dbInit, { type ITestDb } from '../../test/e2e/helpers/database-init.js'; import dbInit, { type ITestDb } from '../../test/e2e/helpers/database-init.js';
import { SYSTEM_USER } from './core.js'; import { SYSTEM_USER } from './core.js';
import getLogger from '../../test/fixtures/no-logger.js'; import getLogger from '../../test/fixtures/no-logger.js';
import { jest } from '@jest/globals';
describe('System user definitions in code and db', () => { describe('System user definitions in code and db', () => {
let dbDefinition: { let dbDefinition: {
@ -13,7 +12,6 @@ describe('System user definitions in code and db', () => {
}; };
let db: ITestDb; let db: ITestDb;
beforeAll(async () => { beforeAll(async () => {
jest.setTimeout(15000);
db = await dbInit('system_user_alignment_test', getLogger); db = await dbInit('system_user_alignment_test', getLogger);
const query = await db.rawDatabase.raw( const query = await db.rawDatabase.raw(

View File

@ -1,16 +0,0 @@
// Partial types for "@unleash/express-openapi".
declare module '@unleash/express-openapi' {
import type { RequestHandler } from 'express';
export interface IExpressOpenApi extends RequestHandler {
validPath: (operation: OpenAPIV3.OperationObject) => RequestHandler;
schema: (name: string, schema: OpenAPIV3.SchemaObject) => void;
swaggerui: RequestHandler;
}
export default function openapi(
docsPath: string,
document: Omit<OpenAPIV3.Document, 'paths'>,
options?: { coerce: boolean; extendRefs: boolean },
): IExpressOpenApi;
}

View File

@ -1,18 +1,18 @@
import { jest } from '@jest/globals'; import { type Mock, vi } from 'vitest';
import { batchExecute } from './batchExecute.js'; import { batchExecute } from './batchExecute.js';
jest.useFakeTimers(); vi.useFakeTimers();
describe('batchExecute', () => { describe('batchExecute', () => {
let mockExecuteFn: jest.Mock; let mockExecuteFn: Mock;
beforeEach(() => { beforeEach(() => {
mockExecuteFn = jest.fn(); mockExecuteFn = vi.fn();
}); });
afterEach(() => { afterEach(() => {
jest.clearAllTimers(); vi.clearAllTimers();
jest.clearAllMocks(); vi.clearAllMocks();
}); });
it('should process each item in batches of the specified size', async () => { it('should process each item in batches of the specified size', async () => {
@ -23,7 +23,7 @@ describe('batchExecute', () => {
batchExecute(items, batchSize, delayMs, mockExecuteFn); batchExecute(items, batchSize, delayMs, mockExecuteFn);
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
jest.advanceTimersByTime(delayMs); vi.advanceTimersByTime(delayMs);
await Promise.resolve(); await Promise.resolve();
} }
@ -42,11 +42,11 @@ describe('batchExecute', () => {
expect(mockExecuteFn).toHaveBeenCalledTimes(5); expect(mockExecuteFn).toHaveBeenCalledTimes(5);
jest.advanceTimersByTime(delayMs); vi.advanceTimersByTime(delayMs);
await Promise.resolve(); await Promise.resolve();
expect(mockExecuteFn).toHaveBeenCalledTimes(10); expect(mockExecuteFn).toHaveBeenCalledTimes(10);
jest.advanceTimersByTime(delayMs); vi.advanceTimersByTime(delayMs);
await Promise.resolve(); await Promise.resolve();
expect(mockExecuteFn).toHaveBeenCalledTimes(15); expect(mockExecuteFn).toHaveBeenCalledTimes(15);
}); });

View File

@ -9,6 +9,7 @@ function registerGracefulShutdown(unleash: IUnleash, logger: Logger): void {
logger.info('Unleash has been successfully stopped.'); logger.info('Unleash has been successfully stopped.');
process.exit(0); process.exit(0);
} catch (e) { } catch (e) {
console.log('Exiting with code 1');
logger.error('Unable to shutdown Unleash. Hard exit!'); logger.error('Unable to shutdown Unleash. Hard exit!');
process.exit(1); process.exit(1);
} }

View File

@ -3,7 +3,7 @@ import { createTestConfig } from '../../test/config/test-config.js';
import { compareAndLogPostgresVersion } from './postgres-version-checker.js'; import { compareAndLogPostgresVersion } from './postgres-version-checker.js';
import FakeSettingStore from '../../test/fixtures/fake-setting-store.js'; import FakeSettingStore from '../../test/fixtures/fake-setting-store.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
let config: IUnleashConfig; let config: IUnleashConfig;
let settingStore: ISettingStore; let settingStore: ISettingStore;
@ -12,7 +12,7 @@ let errorMessages: string[];
const fakeSettingStore = (postgresVersion: string): ISettingStore => { const fakeSettingStore = (postgresVersion: string): ISettingStore => {
const temp = new FakeSettingStore(); const temp = new FakeSettingStore();
jest.spyOn(temp, 'postgresVersion').mockResolvedValue(postgresVersion); vi.spyOn(temp, 'postgresVersion').mockResolvedValue(postgresVersion);
return temp; return temp;
}; };

View File

@ -1,5 +1,5 @@
import timer from './timer.js'; import timer from './timer.js';
import { jest } from '@jest/globals'; import { vi } from 'vitest';
function timeout(fn, ms): Promise<void> { function timeout(fn, ms): Promise<void> {
return new Promise((resolve) => return new Promise((resolve) =>
@ -18,14 +18,14 @@ test('should calculate the correct time in seconds', () => {
}); });
test('timer should track the time', async () => { test('timer should track the time', async () => {
jest.useFakeTimers(); vi.useFakeTimers();
const tt = timer.new(); const tt = timer.new();
let diff: number | undefined; let diff: number | undefined;
timeout(() => { timeout(() => {
diff = tt(); diff = tt();
}, 20); }, 20);
jest.advanceTimersByTime(20); vi.advanceTimersByTime(20);
expect(diff).toBeGreaterThan(0.0019); expect(diff).toBeGreaterThan(0.0019);
expect(diff).toBeLessThan(0.05); expect(diff).toBeLessThan(0.05);
jest.useRealTimers(); vi.useRealTimers();
}); });

View File

@ -28,7 +28,6 @@ const initializeTemplateDb = (db: IUnleashConfig['db']): Promise<void> => {
); );
await Promise.all( await Promise.all(
result.rows.map((row) => { result.rows.map((row) => {
console.log(`Dropping test database ${row.datname}`);
return client.query(`DROP DATABASE ${row.datname}`); return client.query(`DROP DATABASE ${row.datname}`);
}), }),
); );

Some files were not shown because too many files have changed in this diff Show More