Service for managing disabled E2E tests in a CI/CD pipeline. CI calls POST /resolve before running E2E tests and receives a list of tests to skip.
docker compose up --build # first start / rebuild
docker compose down && docker compose up --build # restart
downpreserves MongoDB data (named volume). The-vflag removes the volume together with its data.
Service: http://localhost:3000 | Swagger UI: http://localhost:3000/docs
npm run docker:mongo:dev # single docker-compose.yml: only the mongo-dev service (local-mongo profile)
npm run dev # or: npm run dev:local - first Mongo, then dev serverManually: docker compose up -d mongo-dev. A regular docker compose up does not touch the mongo-dev service (it is behind the local-mongo profile). Do not run docker compose --profile local-mongo up -d without a service list - it will start both the full stack (mongo1...app) and mongo-dev.
By default, src/config.ts uses mongodb://localhost:27017, so a separate .env file is not required. Example variables are in .env.example (set them in your shell or through your IDE mechanism).
To stop only the dev Mongo: npm run docker:mongo:dev:down (data in the mongo_dev_data volume is preserved; to fully reset the volume: docker volume rm ...).
npm test # unit tests (Jest + supertest, MongoDB is mocked)
npm run test:coverage # unit + coverage (threshold: 100%)
npm run test:integration # integration tests (mongodb-memory-server, Docker not required)
npm run test:all # unit + integrationBuild script (npm run build): format:check -> test:coverage -> swagger -> tsc. The Docker build fails if the code is not formatted or coverage is below 100%.
200: { "status": "ok", "mongo": "connected" } | 503: { "status": "degraded", "mongo": "unavailable" }
{ "testId": "TP000001", "targetBranch": "MASTER", "reason": "Flaky, ticket JIRA-1234", "author": "john.doe" }201 - created. 409 - duplicate testId + targetBranch.
Returns all filters for a branch. Optionally supports &reason=JIRA-1234 (regexp).
Get / delete a filter by ObjectId. DELETE returns 204.
{ "reason": "Updated: fix deployed, still flaky", "author": "jane.doe" }testId and targetBranch cannot be changed - that would be a different filter.
{ "targetBranch": "MASTER" }{ "targetBranch": "MASTER", "disabledTests": ["TP000001", "TP000007"], "resolvedAt": "..." }Branch logic: MASTER -> filters for MASTER + PROD. PROD -> only PROD.
On MongoDB error: 200, "disabledTests": [], "fallback": true (fail-open).
{ "error": "Filter not found" }
{ "error": "Validation failed", "details": [{ "field": "testId", "message": "Required" }] }/resolve returns an array of testId values. CI builds the runner CLI arguments on its own.
The alternative - returning a ready-made grep pattern - would couple the service to Mocha and break the contract if the runner changes.
CI converts the array into --grep "TP000001|TP000007" --invert. An empty disabledTests means all tests run without extra flags.
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST http://test-filter-service/resolve \
-H "Content-Type: application/json" -d "{\"targetBranch\": \"$TARGET_BRANCH\"}")
HTTP_STATUS=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n -1)
if [ "$HTTP_STATUS" != "200" ]; then
echo "WARNING: Service unavailable. Running all tests (fail-open)."
GREP_ARGS=""
else
DISABLED=$(echo "$BODY" | jq -r '.disabledTests | join("|")')
GREP_ARGS=$([ -n "$DISABLED" ] && echo "--grep \"$DISABLED\" --invert" || echo "")
fi
npx mocha 'tests/**/*.spec.js' $GREP_ARGSWhy --grep instead of .mocharc: filters are dynamic, so they cannot be fixed in config.
Limitations: the TP###### format is safe for regex; with more than 100 filters the grep string becomes long - this can be solved by using the Mocha programmatic API with a .mocharc.js file generated by CI from the service response.
- The service does not control the runner directly - it returns a list of
testIdvalues, and CI applies that list toMochaor another runner. PRODis a subset ofMASTER- thereforeMASTERuses filters forMASTER + PROD, whilePRODuses onlyPROD.- Fail-open is preferred over fail-close - if MongoDB is unavailable, CI should continue and run all tests instead of stopping the pipeline.
testIdis a stable test identifier - tests are assumed to have unique and persistent IDs in theTP######format so they can be reliably disabled.- Filters must survive service restarts - therefore in-memory storage is not considered the main solution.
- Graceful shutdown - when stopping, the service waits for current requests to finish and closes the MongoDB connection. Timeout: 10 seconds, then forced exit.
testIdandtargetBranchcannot be changed -PATCHonly updatesreasonandauthor. To change the test or branch, delete the filter and create a new one./resolvereturns a stable response - thedisabledTestslist is deduplicated and sorted so identical requests produce identical results.- Physical deletion -
DELETEremoves the document permanently, with no soft delete and no history. - MongoDB is the only dependency - no Redis, queues, or other services.
- Indexes are created on startup -
ensureIndexes()runs on every start; if indexes already exist, MongoDB skips creation. - Swagger is optional - if
swagger-output.jsonis missing,/docsis simply disabled and the service continues to run.
All input data is described by Zod schemas in src/types.ts. A single schema produces TypeScript types, request validation, and Swagger examples. If a Swagger example does not match the schema, the build fails.
- Single point of failure - if the service is unavailable, CI runs all tests (fail-open).
- No cache - every
/resolvehits MongoDB. If the database is unavailable, the service returns an empty list andfallback: true. - No filter expiration - a forgotten filter will disable a test indefinitely.
- No audit log - when a filter is deleted, information about who created it, why, and when it was deleted is lost.
- No rate limiting.
MongoDB (required by the task). Data is stored in the mongo_data named volume. In-memory storage (Map) is not suitable because filters must survive restarts.
Two indexes:
{ testId: 1, targetBranch: 1 } // unique - one test per branch
{ targetBranch: 1 } // for /resolve: find({ targetBranch: { $in: [...] } })- Caching
/resolve- Redis, so MongoDB is not queried on every CI request. Refresh the cache every 30-60 seconds. - Multiple instances - the service does not keep state in memory, so multiple copies can run behind a load balancer.
- Audit log - a separate
filter_eventscollection with history: who created, updated, or deleted a filter. - Authorization - API keys for CI, JWT for filter management.
- Notifications - Slack/webhook notifications when a filter is created or deleted.
- Regexp-based filtering - a
patternfield for disabling groups of tests by pattern. - Jira integration - a
ticketIdfield and automatic filter removal when the ticket is closed. - API versioning -
/v1/resolve,/v1/filters, so the contract can evolve without breaking old clients.
- Structured logging - add a request ID to link service logs with a specific CI run.
- Metrics - a
/metricsendpoint (Prometheus): request counters, response time, number of active filters. - CI pipeline - separate stages: lint -> tests -> build -> deploy. Right now everything runs only inside
docker build. - Alerting on fallback - when MongoDB is unavailable and
/resolvereturns an empty list, send a Slack alert so the team learns about the issue before flaky tests start breaking the pipeline. - ReDoS protection - the
reasonparameter inGET /filtersis passed to MongoDB as regex. A complex pattern can hang the query. Fix: limit pattern length, validate safety (safe-regex2), add a query timeout (maxTimeMS). - Bulk operations -
POST /filters/bulkandDELETE /filters/bulkto create/delete multiple filters in one request. - Write locking - so
/resolvedoes not read filters while they are being modified. This guarantees that all CI jobs receive either the old state or the new state in full, without mixing them.