diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b3e78081..99bd77458 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,6 +138,7 @@ jobs: node-version: 23 registry-url: 'https://registry.npmjs.org' - run: npm install + working-directory: ./webui - name: Install taskfile run: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin - name: Publish diff --git a/.gitignore b/.gitignore index 79316fd7f..0c8b28c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ data/* dist/ bin/* +obj/ +Debug/ node_modules diff --git a/acceptance/petstore_test.go b/acceptance/petstore_test.go index 6e66831fc..36eacb298 100644 --- a/acceptance/petstore_test.go +++ b/acceptance/petstore_test.go @@ -293,12 +293,16 @@ func (suite *PetStoreSuite) TestEvents() { try.AssertBody(func(t *testing.T, body string) { var data map[string]any err := json.Unmarshal([]byte(body), &data) - require.NoError(t, err) + assert.NoError(t, err) results := data["results"].([]any) - evt := results[0].(map[string]any) - require.Equal(t, "Event", evt["type"]) - require.Equal(t, "GET http://127.0.0.1:18080/user/bob", evt["title"]) - require.Equal(t, "Swagger Petstore", evt["domain"]) + assert.NotNil(t, results, "search result should contain results") + assert.Greater(t, len(results), 0) + evt, ok := results[0].(map[string]any) + assert.True(t, ok, "event should be a map[string]any") + assert.NotNil(t, evt) + assert.Equal(t, "Event", evt["type"]) + assert.Equal(t, "GET http://127.0.0.1:18080/user/bob", evt["title"]) + assert.Equal(t, "Swagger Petstore", evt["domain"]) }), ) } diff --git a/api/handler_search_test.go b/api/handler_search_test.go index 0089cd404..cc7905489 100644 --- a/api/handler_search_test.go +++ b/api/handler_search_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "mokapi/config/dynamic" "mokapi/config/dynamic/asyncApi/asyncapitest" @@ -10,6 +11,7 @@ import ( "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "mokapi/try" "net/http" "testing" @@ -43,7 +45,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -62,7 +65,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -81,7 +85,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -102,7 +107,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -131,7 +137,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -160,7 +167,8 @@ func TestHandler_SearchQuery(t *testing.T) { }, app: func() *runtime.App { app := runtime.New(&static.Config{Api: static.Api{Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) h := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) @@ -180,7 +188,12 @@ func TestHandler_SearchQuery(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - h := New(tc.app(), static.Api{}) + app := tc.app() + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + + h := New(app, static.Api{}) try.Handler(t, http.MethodGet, diff --git a/api/handler_test.go b/api/handler_test.go index 918ba383c..9d0d4716a 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -196,7 +196,7 @@ func TestHandler_NoDashboard(t *testing.T) { } func TestHandler_SearchEnabled(t *testing.T) { - h := api.New(runtime.New(&static.Config{}), static.Api{Dashboard: true, Search: static.Search{Enabled: true}}) + h := api.New(runtime.New(&static.Config{}), static.Api{Dashboard: true, Search: static.Search{Enabled: true, InMemory: true}}) try.Handler(t, http.MethodGet, "http://foo.api/api/info", diff --git a/cmd/mokapi/main_test.go b/cmd/mokapi/main_test.go index 47acd92f1..81e1868fb 100644 --- a/cmd/mokapi/main_test.go +++ b/cmd/mokapi/main_test.go @@ -82,6 +82,8 @@ api: dashboard: true search: enabled: true + indexPath: "" + inMemory: false rootCaCert: "" rootCaKey: "" configs: [] diff --git a/config/static/static_config.go b/config/static/static_config.go index a29557435..85b05813e 100644 --- a/config/static/static_config.go +++ b/config/static/static_config.go @@ -66,7 +66,9 @@ type Api struct { } type Search struct { - Enabled bool + Enabled bool + IndexPath string `yaml:"indexPath" json:"indexPath" flag:"index-path"` + InMemory bool `yaml:"inMemory" json:"inMemory" flag:"in-memory"` } type FileProvider struct { diff --git a/docs/config.json b/docs/config.json index a546cbf60..75f4cbb4c 100644 --- a/docs/config.json +++ b/docs/config.json @@ -588,9 +588,9 @@ }, "items": [ { - "label": "Ensuring API Contract Compliance with Mokapi", + "label": "Guard Your API Contracts: Catch Breaking Changes Before Production", "source": "resources/blogs/ensuring-api-contract-compliance-with-mokapi.md", - "path": "/resources/blogs/ensuring-api-contract-compliance-with-mokapi", + "path": "/resources/blogs/guard-your-api-contracts", "hideNavigation": true }, { diff --git a/docs/javascript-api/mokapi/eventhandler/eventargs.md b/docs/javascript-api/mokapi/eventhandler/eventargs.md index 447b6c321..804b91d33 100644 --- a/docs/javascript-api/mokapi/eventhandler/eventargs.md +++ b/docs/javascript-api/mokapi/eventhandler/eventargs.md @@ -8,12 +8,34 @@ description: EventArgs is an object used to configure event handlers registered [`on`](/docs/javascript-api/mokapi/on.md) function when registering an event handler. It allows controlling how and when an event handler is executed. -| Name | Type | Description | -|----------|---------|--------------------------------------------------------------------------------------------------------| -| tags | object | Adds or overrides existing tags that are used in dashboard | -| priority | integer | Defines the execution priority of the event handler. Handlers with a higher value are executed first. | - -If no priority is specified, the default priority is `0`. +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tagsobjectAdds or overrides existing tags that are used in dashboard.
trackboolean |
(params) => boolean
Controls whether this event handler is tracked in the dashboard.
priorityintegerDefines the execution priority of the event handler. Handlers with a higher value are executed first. If no priority is specified, the default priority is 0.
+
## Example: Adding custom tags @@ -29,6 +51,62 @@ export default function() { } ``` +## Example: Controlling whether an event handler is tracked in the dashboard + +The track field controls whether executions of an event handler appear in the dashboard. + +It supports two modes: +- true / false — always track or never track +- a function — decide dynamically per request + +### Static tracking + +```javascript +on('http', handler, { track: true }) +``` + +### Dynamic tracking (function) + +```javascript +on('http', handler, { + track: (request, response) => request.key !== '/health' +}) +``` + +### Full example: Delay simulation with tracking + +```javascript +import { on, sleep } from 'mokapi' + +export default () => { + let delay = undefined + + on('http', (request, response) => { + if (request.key === '/simulations/delay') { + switch (request.method) { + case 'PUT': + delay = request.query.duration; + break; + case 'DELETE': + delay = undefined; + break; + } + } + }, { track: true }) + on('http', (request, response) => { + if (!delay) { + sleep(delay); + } + }, { track: () => delay !== undefined } ) +} +``` + +#### Explanation +- The first handler exposes a control endpoint to configure the delay. + - track: true ensures all configuration changes are visible in the dashboard. +- The second handler applies the delay to incoming requests. + - Tracking is enabled only while a delay is active, keeping the dashboard clean when no simulation is running. + ## Example: Controlling execution order with priority When multiple handlers are registered for the same event, the priority diff --git a/docs/resources/blogs/acceptance-testing.md b/docs/resources/blogs/acceptance-testing.md index c248b5d9c..80e2e3647 100644 --- a/docs/resources/blogs/acceptance-testing.md +++ b/docs/resources/blogs/acceptance-testing.md @@ -1,136 +1,186 @@ --- title: "Acceptance Testing with Mokapi: Focus on What Matters" description: Discover how Mokapi simplifies acceptance testing with mock APIs for REST or Kafka. Stay aligned with specs, handle edge cases, and test with confidence. +subtitle: Build confidence in your software with acceptance tests that validate behavior, not implementation. Learn how Mokapi makes testing against external APIs practical and maintainable. image: url: "/acceptance-testing.png" alt: Diagram illustrating acceptance testing as executable specifications interacting with the backend and mocked external APIs using Mokapi. +tags: [ Testing, CI/CD, Quality] +links: + items: + - title: CI/CD Integration Guide + href: /resources/tutorials/running-mokapi-in-a-ci-cd-pipeline + - title: Guard Your API Contracts + href: /resources/blogs/guard-your-api-contracts --- # Acceptance Testing with Mokapi: Focus on What Matters -In today’s fast-paced development cycles, it’s crucial to ensure your software meets real user -expectations. While unit tests validate internal code, acceptance testing answers the bigger question: -Does the system behave as users expect? This post explores the value of acceptance tests and -how Mokapi supports it by simulating APIs, validating specifications, and enabling robust, -realistic test cases. - - Diagram illustrating acceptance testing as executable specifications interacting with the backend and mocked external APIs using Mokapi. -## Why Acceptance Testing - -**Software testing is not merely a box to check—it is a fundamental process to answer one critical question: Is our software releasable?** - -Among the various levels of testing, acceptance testing offers the most direct insight into whether the software meets business and user expectations. It bridges the gap between the world of users and the inner workings of the code by turning expectations into **precise, executable checks** on system behavior. +In fast-paced development cycles, it's crucial to ensure your software meets real user expectations. +Unit tests validate that individual components work correctly, but they can't answer the bigger question: +*Does the system behave the way users expect?* -### The Nature of Acceptance Tests +That's where acceptance testing comes in, and it's more than just another layer of testing. +It's a fundamental shift in how we think about software quality. -At its core, an **acceptance test** is an *executable specification* of how a system should behave, written from a user’s perspective. It is not concerned with how the system is implemented, but with **what it does**—its behavior and its outcomes. +> Is our software releasable? -While unit tests focus on individual components, acceptance tests focus on the system's intent. They clarify what should happen when a user interacts with the software under certain conditions. +Among all testing levels, acceptance testing offers the most direct insight into whether software meets business and +user expectations. It bridges the gap between user needs and code implementation by turning expectations into **precise**, +**executable specifications** of system behavior. -When we get the level of abstraction right, acceptance tests become **clear, precise, and maintainable**. They reflect scenarios that matter to users, expressed in a way that is both **easy to read and easy to execute**. They serve as living documentation that evolves with the system and its requirements. +## What Makes Acceptance Tests Different -### Solving Ambiguity and Ensuring Reproducibility +### Unit Tests vs. Acceptance Tests -One of the most challenging aspects of software development is not writing code—it is **understanding the problem clearly and expressing it precisely**. Programs are, after all, specifications of what we want the system to do. But unlike natural languages, which are rich in context and ambiguity, software requires **unambiguous clarity**. +**Unit Tests:** +- Focus on *how* components work internally +- Test implementation details +- Fast, isolated, developer-focused +- Break when implementation changes +- Answer: "Does this code work?" -This is where acceptance testing shines. +**Acceptance Tests:** +- Focus on *what* the system does +- Test behavior and outcomes +- Slower, integrated, user-focused +- Stable across implementation changes +- Answer: "Does this system meet expectations?" -By expressing requirements in an executable form, acceptance tests remove ambiguity. They define **exactly** what needs to happen. Rather than relying on vague documentation or human interpretation, developers, testers, and stakeholders can rely on **concrete, shared understanding**. The tests describe *behavior*, not implementation details, which makes them robust to changes in how the system is built, as long as it continues to behave as expected. +### Acceptance Tests as Executable Specifications -Moreover, acceptance tests are **reproducible**. They can be run automatically, as often as needed, to ensure the software continues to meet its defined expectations. This reproducibility is crucial for modern CI pipelines, where tests are run continuously to catch regressions early +At its core, an acceptance test is an *executable specification* of how a system should behave, written from a user's +perspective. It's not concerned with implementation, only with behavior and outcomes. -### A Contract of Trust +When we get the abstraction level right, acceptance tests become **clear, precise, and maintainable**. They reflect +scenarios that matter to users, expressed in a way that's both easy to read and easy to execute. They serve as +living documentation that evolves with the system. -In a collaborative development environment, acceptance tests serve as a contract between the business and the development team. When everyone agrees on the specifications encoded in the tests, there is no room for misinterpretation. The business gets what it asked for, and developers have a reliable guide to follow. +### Removing Ambiguity, Ensuring Reproducibility -This contract is especially important in agile and iterative processes, where requirements evolve and features are delivered incrementally. With acceptance tests in place, the team always has a clear picture of what "done" means. +One of the hardest challenges in software development isn't writing code, it's understanding the problem +clearly and expressing it precisely. Software requires unambiguous clarity, unlike natural language which +thrives on context and implication. -### Making Software Releasable +This is where acceptance testing shines: -Acceptance testing answers a key question: **Is the software ready to release?** -It gives us confidence that the features meet user needs and work correctly. It also checks that we built what was actually requested. Most importantly, it does this in a way that’s **automated**, **repeatable**, and **reliable**. +``` box=benefits title="Removes Ambiguity" emoji=🎯 +By expressing requirements in executable form, tests define exactly what needs to happen, no vague documentation or +human interpretation required. +``` -By aligning development with user intent, removing ambiguity, and validating results, acceptance testing becomes an essential practice—not just for verifying software, but for **clearly understanding and specifying it** in the first place. +``` box=benefits title="Ensures Reproducibility" emoji=🔁 +Tests run automatically, as often as needed, catching regressions early and providing consistent results across environments. +``` -### The problem with acceptance testing and 3rd party APIs +``` box=benefits title="Creates Shared Understanding" emoji=🤝 +Developers, testers, and stakeholders rely on concrete, shared definitions of behavior rather than assumptions. +``` -- **Unstable** – External APIs can fail randomly, whether due to bugs or release workflows. -- **No test environment** – Many APIs don’t offer a dedicated environment for testing. -- **Uncontrollable state** – It’s often impossible to set up the external system in the desired state for your tests. +``` box=benefits title="Serves as a Contract" emoji=📜 +When everyone agrees on specifications encoded in tests, there's no room for misinterpretation between business and development. +``` -These limitations can make acceptance testing brittle and unreliable—unless you can simulate the external system reliably. +## The Problem: External APIs Break Acceptance Tests -## How Mokapi Supports Acceptance Testing +Acceptance tests sound great in theory, but in practice they often depend on external APIs such as payment providers, +authentication services, notification systems, data feeds, etc. These dependencies introduce serious problems: -That’s where Mokapi comes in. It lets you simulate realistic API behavior in a way that’s fast, accurate, and under your full control. +- **Unstable** + External APIs can fail randomly due to bugs, maintenance, or release workflows. Your tests fail even when your code is correct. +- **No Test Environment** + Many third-party APIs don't offer dedicated test environments, forcing you to test against production or skip testing entirely. +- **Uncontrollable State** + It's often impossible to set up external systems in the exact state your test needs such as specific user data, edge cases, error conditions. -Flow diagram showing how executable acceptance tests validate backend behavior with mocked APIs. +These limitations make acceptance testing prone to errors and unreliable. Tests that should actually verify the behavior +of your system instead become tests of whether external APIs happen to be working today. -Acceptance testing becomes significantly more effective with the right tools—especially in complex environments with multiple APIs, microservices, and evolving interfaces. Mokapi was developed to address precisely these challenges. It provides a powerful, flexible, and specification-oriented approach to API simulation, enabling high-quality acceptance testing in a wide variety of scenarios. +> Your acceptance tests shouldn't fail because a third-party API is down. +> They should validate your system's behavior under controlled conditions. -### Acceptance Testing Across Boundaries +## How Mokapi Solves This -Mokapi makes acceptance testing easier across flexible system boundaries—whether you're focusing on a single microservice or validating your entire architecture—by mocking the APIs they depend on. +Mokapi lets you simulate realistic API behavior in a way that's fast, accurate, and under your full control. +It acts as a stand-in for external APIs during testing, allowing you to focus on validating your system's +behavior instead of fighting with unstable dependencies. -Diagram illustrating flexible acceptance testing boundaries with Mokapi. Shows how tests can target a single microservice or span across multiple services by mocking dependent external APIs. +![Flow diagram showing how executable acceptance tests validate backend behavior with mocked APIs.](/acceptance-testing-mokapi.png "Mokapi mocks external dependencies, letting your acceptance tests focus on validating system behavior") -### Powerful Flexibility for Real-World Scenarios +### Flexible Testing Boundaries -With Mokapi, you're not limited to ideal cases. The JavaScript-based mock handlers enable the simulation of a wide variety of real-world scenarios, including negative tests, edge cases, and error conditions—all essential for developing robust software. You can programmatically control responses based on request data, headers, or business logic, giving you precise control over your mock's behavior during a test. +Mokapi simplifies acceptance testing across flexible system boundaries, whether you are focusing on a single +microservice or validating your entire architecture by mocking the APIs they depend on. -This flexibility helps you answer not only the question "Does it work?" but also "Does it work when third-party systems don't." +![Diagram illustrating flexible acceptance testing boundaries with Mokapi. Shows how tests can target a single microservice or span across multiple services by mocking dependent external APIs.](/acceptance-testing-boundaries-mokapi.png "Test a single service in isolation or validate multiple services together by mocking their external dependencies") -### Specification-Driven Confidence +You decide where to draw the boundary. Test a single microservice by mocking everything it calls, or test an +entire subsystem by mocking only the external APIs at the edges. Mokapi adapts to your testing strategy. -One of Mokapi's key strengths is its close alignment with API specifications. Mocks are continuously validated against OpenAPI or AsyncAPI definitions, ensuring they accurately reflect the APIs being simulated. This becomes especially valuable as APIs evolve. +### What Mokapi Brings to Acceptance Testing -When a provider releases a new API version, it can be easily updated in Mokapi. If mock data or API calls no longer conform to the updated specification, Mokapi returns errors that can be caught through acceptance testing. This supports automated API version updates and acts as an early warning system for potential production issues. +- **Realistic Scenarios:** + JavaScript-based handlers let you simulate edge cases, error conditions, and complex workflows that are hard or impossible to trigger with real APIs +- **Specification-Driven Validation:** + Mocks are continuously validated against OpenAPI or AsyncAPI specs, ensuring they accurately reflect the APIs being simulated +- **Smart Patching:** + Override only the data relevant to your test without redefining entire responses, keeping tests focused and maintainable +- **Version Management:** + Update API specifications as providers release new versions; Mokapi catches incompatibilities before production +- **Complete Control:** + Set up exact states, trigger errors, simulate timeouts, and make everything programmable and repeatable. -#### Smart Mock Customization with Patch Mechanism +### Example: Smart Mock Customization - Focus on What Matters -Mokapi supports a patching mechanism for both API specifications and mock data. Patching the specification helps you adopt new API versions more easily while keeping track of your own modifications to the original spec. Patching mock data, on the other hand, allows you to avoid over-mocking—keeping your tests stable and focused, so changes are only required when they’re truly relevant. +With Mokapi, you can generate valid mock data from your OpenAPI specification and customize +only the parts that matter for your test. -Here's an example: +In this example, the mock response is generated automatically, and you override just a single field: ```typescript -import { on, patch } from 'mokapi'; +import { on } from 'mokapi'; import { fake } from 'mokapi/faker'; export default function() { - // Open the OpenAPI pet store specification, resolving all $ref references - const api = open('pet-store-api.yaml', { as: 'resolved' }); - - // Generate a random pet object based on the OpenAPI schema - const pet = fake(api.components.schemas.pet); - on('http', (request, response) => { // Use the generated pet but override the name to 'Odie' - response.data = patch(pet, { name: 'Odie' }); + response.data.name = 'Odie'; - // Alternatively, patch Mokapi's autogenerated response and just set the name + // Alternatively, use Object.assign-style patching // response.data = patch(response.data, { name: 'Odie' }); }); } ``` -This example demonstrates how you can generate valid mock data that conforms to the API specification and then customize only the parts relevant to your test—such as setting a specific pet name—without redefining the entire response structure. +This approach gives you: +- **Schema compliance:** The generated data matches your OpenAPI specification +- **Test focus:** Override only what matters for this specific test (the pet's name) +- **Maintainability:** When the schema changes, only failing patches need updates, not entire mock responses -### Making Acceptance Testing Sustainable +### Early Warning System for API Changes -By combining flexible mocks, specification validation, and a smart patching mechanism, **Mokapi makes acceptance testing more resilient, more focused, and easier to maintain**. It helps your team test confidently—whether you're building a new feature in isolation, validating complex integrations, or preparing for a production release. +When a provider releases a new API version, update the specification in Mokapi. If your mock data or test +calls no longer conform to the updated spec, Mokapi returns validation errors that your acceptance tests +catch immediately. -Mokapi doesn’t just enable acceptance testing—it makes it **practical, maintainable, and deeply aligned with your evolving system and its requirements**. +This becomes an automated API version compatibility check, catching breaking changes before they reach production. -## Ready to get started? +## Making Acceptance Testing Sustainable -Learn how to set up acceptance tests with Mokapi in your CI/CD pipeline: +By combining flexible mocks, specification validation, and smart patching, **Mokapi makes acceptance +testing more resilient, focused, and maintainable**. -👉 [Running Mokapi in a CI/CD Pipeline](/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline) +Your tests answer the right question: "Does our system behave correctly?", without getting derailed by +external API instability, missing test environments, or uncontrollable state. ---- +Mokapi doesn't just enable acceptance testing. It makes it **practical, maintainable, and deeply aligned +with your evolving system and its requirements**. + +## Ready to Build Better Acceptance Tests? + +Learn how to integrate Mokapi into your CI/CD pipeline and start testing with confidence. -*This article is also available on [LinkedIn](https://www.linkedin.com/pulse/acceptance-testing-mokapi-focus-what-matters-marcel-lehmann-fccjf)* +{{ cta-grid key="links" }} ---- \ No newline at end of file diff --git a/docs/resources/blogs/automation-testing-agile-development.md b/docs/resources/blogs/automation-testing-agile-development.md index e4f38752b..1ea91b2f2 100644 --- a/docs/resources/blogs/automation-testing-agile-development.md +++ b/docs/resources/blogs/automation-testing-agile-development.md @@ -1,98 +1,209 @@ --- title: Automation Testing in Agile Development description: Automated tests are key for Agile and CI teams. Discover how Mokapi enables faster, high-quality software development with mocking and testing tools. +subtitle: Discover how automated testing with API mocking enables agile teams to iterate faster, maintain quality, and ship confidently, even with 50+ deployments per day. +tags: [Agile, Testing] +cards: + items: + - title: Get Started With Mokapi + href: /docs/get-started/running + description: Learn how to quickly set up and run your first mock REST API and view the results in the Mokapi dashboard. + - title: Acceptance Testing + href: /resources/blogs/acceptance-testing + description: Discover how Mokapi simplifies acceptance testing + - title: Record & Replay Real Traffic + href: /resources/blogs/record-and-replay-api-interactions + description: Capture real-world API traffic for testing, debugging, and offline development + - title: CI/CD Integration + href: /resources/tutorials/running-mokapi-in-a-ci-cd-pipeline + description: Learn how to use Mokapi in CI/CD pipelines to mock APIs and automate tests + - title: Programmable Scenarios + href: /resources/blogs/bring-your-mock-apis-to-life-with-javascript + description: Mokapi Scripts let you create dynamic, intelligent mock APIs that react to real request data, powered by plain JavaScript. --- -# Automation Testing in Agile Development +# Automation Testing in Agile: Test Faster, Ship Smarter -Automation testing is a critical enabler of agile development, empowering teams -to iterate faster, maintain high-quality standards, and deliver value to users -in shorter timeframes. Mock tools like **Mokapi** amplify these benefits by helping -teams communicate expectations effectively, decouple dependencies, and identify -defects earlier in the development lifecycle. This combination not only streamlines -workflows but also improves time to market. +Agile development thrives on speed and iteration. Teams release smaller increments of code more frequently, +respond to feedback faster, and deliver value to users in shorter cycles. But this velocity creates a critical +challenge: *how do you maintain quality when deploying 10, 20, or even 50 times per day?* -## The Need for Automated Testing in Agile Development +The answer is automation testing and specifically, **automated testing with intelligent API mocking**. +Without it, agile becomes unsustainable. -In an agile development process, software systems are continuously evolving. New -features are added, and changes are made in short iterations. Without an automated -testing framework, maintaining the quality of a product becomes a daunting task. -Automated tests ensure that the software remains flexible and easy to change, even -as its complexity grows. +## Why Manual Testing Fails in Agile -### The Challenges of Manual Testing +Manual testing might work for early-stage development, but it quickly becomes a bottleneck as projects scale +and release cadence accelerates. Here's why: -Manual testing may work for early-stage development but quickly becomes a -bottleneck as projects scale. Agile teams aim to deliver smaller increments of -code, release updates more often, and deploy new features frequently — sometimes -as often as 50 production changes per day. Manual testing cannot keep up with this -pace. It doesn't scale and is incompatible with the demands of continuous delivery. +- **Doesn't Scale:** + As your codebase grows and features multiply, manual testing becomes exponentially slower and more expensive. +- **Can't Keep Pace:** + Agile teams deploy multiple times per day. Manual testers can't validate every change before it ships. +- **Human Error:** + Manual tests are inconsistent. Testers miss edge cases, skip steps under pressure, or introduce variability in results. +- **Blocks Innovation:** + Teams become afraid to change code when they know it requires hours of manual regression testing. -### Continuous Testing for Continuous Delivery - -To support continuous delivery, we need continuous testing. Automated tests ensure -that each code increment is thoroughly validated before reaching production, -eliminating the risk of introducing bugs to end users. This proactive approach -transforms the fear of change into confidence, allowing teams to innovate without -hesitation. +In agile environments, manual testing transforms from a quality assurance process into a **delivery blocker**. +The very thing meant to ensure quality ends up slowing down the feedback loop and frustrating developers. > Write tests until fear is transformed into boredom > Kent Beck -## Why Contract Testing Matters - -Modern software systems are more complex than ever, often integrating with multiple -external services maintained by other teams. Testing these integrations presents -unique challenges. External systems may change unexpectedly, introduce bugs, or -even be unavailable during testing, making end-to-end testing unreliable and -time-consuming. - -### The Role of Contract Testing +## The Agile Testing Mandate: Continuous Testing for Continuous Delivery + +To support continuous delivery, we need continuous testing. Every code change must be automatically +validated before reaching production. This isn't optional, it's the only way to maintain quality at agile velocity. + +Automated tests provide: + +``` box=benefits title=Speed emoji=⚡ +Tests run in seconds or minutes, not hours or days. Feedback is immediate, enabling rapid iteration. +``` + +``` box=benefits title=Repeatability emoji=🔁 +Tests produce consistent results every time. No human variability, no forgotten steps. +``` + +``` box=benefits title=Confidence emoji=🛡️ +Teams deploy without fear, knowing tests catch regressions before production. +``` + +``` box=benefits title=Scalability emoji=📈 +Test suites grow with your codebase, covering more scenarios without additional manual effort. +``` + +This proactive approach transforms the *fear of change* into *confidence in quality*. +Teams innovate freely, knowing their test suite acts as a safety net. + +## The Integration Testing Problem + +Modern software systems are rarely standalone. They integrate with payment providers, authentication +services, notification systems, data warehouses, third-party APIs, and internal microservices. +Testing these integrations presents unique challenges: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ChallengeImpact on Testing Solved by Mocking?
External APIs can be downTests fail even when your code is correct
No test environments providedForced to test against production or skip testing
Uncontrollable external stateCan't set up specific test scenarios
Slow external responsesTest suite takes hours to complete
External APIs change unexpectedlyBreaking changes caught in production
+
-Contract testing solves this problem by focusing solely on the system you are -responsible for. Instead of testing the entire ecosystem, you simulate interactions -with external systems, allowing you to verify that your system adheres to the -agreed-upon contracts with its dependencies. +End-to-end testing against real external systems is **unreliable, slow, and often impossible**. +This is where contract testing and API mocking become essential. -Key Benefits of Contract Testing: +### Contract Testing: Focus on What You Control -- Isolation: Test your system independently of external dependencies. -- Speed: Avoid delays caused by dependency availability or state management. -- Resilience: Eliminate runtime failures caused by changes in external systems. +Contract testing solves this by focusing solely on the system you're responsible for. +Instead of testing the entire ecosystem, you: -## How Mokapi Supports Agile Testing +- **Simulate external systems** using their published contracts (OpenAPI, AsyncAPI) +- **Verify your system adheres to contracts** when calling external APIs +- **Test independently** without waiting for external services to be available +- **Control the test environment** completely, enabling reproducible tests for edge cases and error conditions -Mokapi makes it easy to simulate external systems, such as REST APIs or -Apache Kafka topics, enabling controlled and reliable tests. With Mokapi, you -can decouple your testing from external services, reducing complexity and -increasing the efficiency of your test suite. +*You test what you own. You mock what you don't.* -### End-to-End Testing with Mock Tools +## How Mokapi Enables Agile Testing -To create stable tests, all interactions with external systems must be simulated. -By using Mokapi, you can: +Mokapi makes contract testing and API mocking practical for agile teams. It provides the speed, +flexibility, and specification-driven validation needed to test confidently at high velocity. -- Simulate APIs and message queues like Kafka. -- Customize responses to reflect real-world scenarios. -- Focus testing efforts on your system under test (SUT), rather than the entire system. +What Mokapi Brings to Your Test Suite: -## The Impact of Automated Testing with Mock Tools +``` box=feature title="Mock Any API or Message Queue" emoji=🎭 +Simulate REST APIs, GraphQL, Kafka topics, and more using OpenAPI and AsyncAPI specifications. Your tests interact with realistic mock services instead of brittle test doubles. +``` -By using tools like Mokapi into your testing strategy can dramatically improve your team’s -efficiency and product quality: +``` box=feature title="Fast, Isolated Tests" emoji=⚡ +No network calls to external services means tests run in milliseconds, not seconds or minutes. Your CI pipeline completes faster, giving developers immediate feedback. +``` + +``` box=feature title="Specification-Driven Validation" emoji=🎯 +Mokapi validates all requests and responses against your OpenAPI/AsyncAPI specs. Catch contract violations before they reach production. +``` + +``` box=feature title="Programmable Scenarios" emoji=🔧 +Use JavaScript to simulate edge cases, timeouts, error conditions, and complex workflows that are hard or impossible to trigger with real APIs. +``` + +``` box=feature title="Record & Replay Real Traffic" emoji=🔄 +Capture real API interactions from staging or production, then replay them in tests. Build test suites from actual user behavior. +``` + +``` box=feature title="CI/CD Integration" emoji=🚀 +Run Mokapi in Docker, Kubernetes, or GitHub Actions. It fits seamlessly into existing CI/CD pipelines without infrastructure changes. +``` + +## Example: Testing a Payment Integration + +Imagine testing a checkout flow that integrates with Stripe. Without mocking: + +- You need test API keys and a Stripe test account +- Tests can fail if Stripe's API is down or slow +- You can't easily simulate declined cards, timeouts, or rate limits +- Tests are slow due to network round trips + +**With Mokapi:** + +- Define Stripe's API contract using their OpenAPI spec +- Mock successful charges, declined cards, and error responses programmatically +- Tests run in milliseconds with no external dependencies +- Simulate edge cases that would be impossible to trigger with the real API + +*Your tests focus on your checkout logic, not on whether Stripe is working today.* + +## From Fear to Confidence + +Automation testing isn't optional in agile development, it's the foundation that makes continuous delivery +possible. Without it, teams slow down, quality suffers, and deployments become risky events instead +of routine operations. + +Tools like Mokapi take this further by solving the integration testing problem. By mocking external +dependencies, validating contracts, and enabling fast, isolated tests, Mokapi empowers teams to: -- **Speed:** Identify and resolve defects faster, reducing overall testing time. -- **Accuracy:** Gain more reliable test results by simulating controlled environments. -- **Efficiency:** Streamline testing processes, allowing your team to focus on core development tasks. -- **Quality:** Deliver higher-quality software to users faster and with greater confidence. +- Test faster without sacrificing coverage +- Deploy confidently knowing tests catch regressions +- Simulate scenarios impossible to reproduce with real APIs +- Maintain quality even as system complexity grows -## Conclusion +Whether you're building REST APIs, event-driven architectures, or complex microservices, *Mokapi +equips your team to test better, faster, and smarter*. -Automation testing is no longer optional in the world of agile development. It is -a necessity that ensures teams can adapt to change, innovate quickly, and deliver -value to users consistently. Tools like Mokapi take this a step further by enabling -seamless integration testing through mocking and contract testing, empowering teams -to maintain quality even in the face of complexity. +## Ready to Transform Your Testing? -Whether you're developing REST APIs, event-driven architectures, or complex microservices, Mokapi equips your team with the tools to test better, faster, and smarter. +See how Mokapi enables fast, reliable automated testing for agile teams shipping multiple times per day. -Start transforming your testing process with Mokapi today! +{{ card-grid key="cards" }} \ No newline at end of file diff --git a/docs/resources/blogs/end-to-end-testing-mocked-apis.md b/docs/resources/blogs/end-to-end-testing-mocked-apis.md index 9d5b6b993..4aefbf375 100644 --- a/docs/resources/blogs/end-to-end-testing-mocked-apis.md +++ b/docs/resources/blogs/end-to-end-testing-mocked-apis.md @@ -139,12 +139,12 @@ export default () => { break; } } - }) + }, { track: true }) on('http', (request, response) => { if (!delay) { sleep(delay); } - }, { track: true } ) + }, { track: () => delay !== undefined } ) } ``` diff --git a/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md b/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md index bf7e07aa3..3536aef57 100644 --- a/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md +++ b/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md @@ -1,63 +1,77 @@ --- -title: Ensure API Contract Compliance with Mokapi Validation -description: Validate HTTP API requests and responses with Mokapi to catch breaking changes early and keep backend implementations aligned with your OpenAPI spec. +title: "Guard Your API Contracts: Catch Breaking Changes Before Production" +description: Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends +subtitle: Stop API drift in its tracks. Use Mokapi as a validation layer to enforce OpenAPI contracts between clients and backends, no matter who's calling or what they're building. image: url: /mokapi-using-as-proxy.png alt: Flow diagram illustrating how Mokapi enforces OpenAPI contracts between clients, Playwright tests, and backend APIs. tech: http +links: + items: + - title: Get Started with Mokapi + href: /docs/get-started/installation + - title: Record & Replay API Traffic + href: /resources/blogs/record-and-replay-api-interactions --- -# Ensuring Compliance with the HTTP API Contract Using Mokapi for Request Forwarding and Validation +# Guard Your API Contracts: Catch Breaking Changes Before Production -In modern distributed systems, APIs are everywhere — frontend-to-backend, -backend-to-backend, microservices communicating internally, mobile apps, test -automation tools, and more. Each interaction relies on a shared API contract, -often expressed through an OpenAPI specification. Even small -deviations can introduce bugs, break integrations, or slow down development. +In modern distributed systems, APIs are everywhere, frontend to backend, service +to service, mobile apps, test automation, third-party integrations. Each interaction +relies on a shared API contract, typically expressed through an OpenAPI specification. +But what happens when that contract drifts? -By placing Mokapi between a client and a backend, you can ensure that every -**request and response adheres to your OpenAPI specification**. With a few lines -of JavaScript, Mokapi can forward requests to your backend while validating both -sides of the interaction. This provides a powerful way to enforce API correctness — -whether the client is a browser, Playwright tests, your mobile app, or even -another backend service. +A renamed field breaks the mobile app. A missing validation lets bad data into production. +An unexpected status code crashes the frontend. Small deviations compound into debugging +nightmares, broken integrations, and slowed development velocity. -In this article, I explore how Mokapi can act as **a contract-enforcing validation layer** -and why this approach benefits frontend developers, backend teams, QA engineers, -and platform engineers alike. +> What if every API interaction was automatically validated against your OpenAPI spec? +> That's exactly what Mokapi enables, a lightweight validation layer that sits between +> client and backend, enforcing contract compliance on both sides of every request. -Flow diagram illustrating how Mokapi enforces OpenAPI contracts between clients, Playwright tests, and backend APIs. +## The Problem: API Drift is Inevitable -## How to Use Mokapi for API Validation with Request Forwarding? +Even with the best intentions, API contracts drift from reality: -Mokapi cannot only be used for mocking APIs, but it can also sit between any -consumer and a backend service to validate real traffic. Using a small -JavaScript script, Mokapi can forward requests to your backend and -validates both requests and responses. +- **Backend changes without updating the spec:** A developer renames a field, changes a response structure, or adds a new required parameter, but forgets to update the OpenAPI documentation. +- **Frontend assumes behavior that was never specified:** A client sends malformed requests or expects fields that don't exist, and the backend silently accepts it (for now). +- **Microservices evolve independently:** Service A updates its contract, but Service B keeps calling the old version. Everything works until it doesn't. +- **Tests pass, but production breaks:** E2E tests mock the API instead of hitting the real backend, masking contract violations until deployment. + +The result? Teams spend hours debugging "why is this suddenly broken?" instead of shipping features. + +## The Solution: Mokapi as a Contract Guardian + +Mokapi can sit between any client and backend to validate every request and response against your OpenAPI specification. +With a simple JavaScript forwarding script, Mokapi becomes a transparent validation layer that: + +- Blocks invalid requests before they reach your backend +- Validates backend responses before they reach the client +- Provides clear, actionable error messages when violations occur +- Works with browsers, test frameworks, mobile apps, and service-to-service traffic + +![Flow diagram showing Mokapi positioned between clients and backend APIs, validating requests and responses](/mokapi-using-as-proxy.png "Mokapi validates all traffic against your OpenAPI spec, catching contract violations before they cause problems") + +## How It Works: Request Forwarding with Validation + +The core concept is simple: Mokapi intercepts HTTP traffic, validates it against your OpenAPI spec, +forwards valid requests to the backend, validates the response, and only then returns it to the client. + +Here's the complete forwarding and validation script: ```typescript import { on } from 'mokapi'; import { fetch } from 'mokapi/http'; /** - * This script demonstrates how to forward incoming HTTP requests - * to a real backend while letting Mokapi validate responses according - * to your OpenAPI spec. - * - * The script listens to all HTTP requests and forwards them based - * on the `request.api` field. Responses from the backend are - * validated when possible, and any errors are reported back to - * the client. + * Forward incoming requests to backend services while validating + * both requests and responses against OpenAPI specifications. */ export default async function () { - - /** - * Register a global HTTP event handler. - * This function is called for every incoming request. - */ + on('http', async (request, response) => { - // Determine the backend URL to forward this request to + // Map request to backend URL based on OpenAPI spec name const url = getForwardUrl(request) // If no URL could be determined, return an error immediately @@ -76,7 +90,7 @@ export default async function () { timeout: '30s' }); - // Copy status code and headers from the backend response + // Copy status code and headers response.statusCode = res.statusCode; response.headers = res.headers @@ -121,146 +135,108 @@ export default async function () { } ``` -For each interaction, Mokapi performs four important steps: - -### 1. Validates incoming requests - -Mokapi checks every incoming request against your OpenAPI specification: - -- HTTP method -- URL & parameters -- headers -- request body - -If the client sends anything invalid, Mokapi blocks it and returns a clear -validation error. - -### 2. Forwards valid requests to your backend +### The Four-Step Validation Flow -If the request is valid, Mokapi forwards it unchanged to the backend using JavaScript. +#### Validate Incoming Request -- No changes are required in your backend. -- No additional infrastructure is necessary. +Mokapi checks the HTTP method, URL parameters, headers, and request body against your OpenAPI spec. +Invalid requests are blocked with clear error messages. -### 3. Validates backend responses +#### Forward Valid Requests -Once the backend responds, Mokapi validates the response against the OpenAPI specification: +Only requests that pass validation are forwarded to the backend. No changes to your backend code +or infrastructure are required. -- status codes -- headers -- response body +#### Validate Backend Response -If something doesn't match the contract, Mokapi blocks it and sends a validation error back to the client. +Mokapi validates the backend's response, status codes, headers, and response body against the OpenAPI specification. -### 4. Return the validated response to the client +#### Return Validated Response -Only responses that pass validation reach the client, guaranteeing contract fidelity end-to-end. +Only responses that match the contract reach the client, guaranteeing end-to-end contract fidelity across every interaction. -## Where You Can Use Mokapi for Request Forwarding and Validation +## Where to Use Mokapi for Contract Validation -Mokapi’s forwarding and validation capabilities make it useful far beyond local development or Playwright scripting. +Mokapi's forwarding and validation capabilities work in multiple scenarios across your architecture: -### Between Frontend and Backend +### Frontend ↔ Backend -Placing Mokapi between your frontend and backend ensures: -- automatic request and response validation -- immediate detection of breaking changes -- backend and API specification evolve together -- fewer “why is the frontend broken?” debugging loops +Place Mokapi between your frontend and backend to catch contract violations during development: +- Automatic request and response validation +- Immediate detection of breaking changes +- Backend and OpenAPI spec evolve together +- Fewer "why is the frontend broken?" debugging loops -Frontend developers can experiment with confidence, knowing the backend -cannot silently diverge from the published contract. +### Service ↔ Service -### Between Backend Services (Service-to-Service) +In microservice architectures, API drift between services causes instability. Mokapi provides: +- Strict contract enforcement between services +- Early detection of incompatible changes +- Stable integrations as teams evolve independently +- Clear validation errors during development and CI -In microservice architectures, API drift between services is a frequent cause of instability. -Routing service-to-service traffic through Mokapi gives you: -- strict contract enforcement between services -- early detection of incompatible changes -- stable integrations even as teams evolve independently -- clear validation errors during development and CI +### Playwright Tests ↔ Backend -Mokapi becomes a lightweight, spec-driven contract guardian across your backend ecosystem. +One of the most powerful setups: Playwright → Mokapi → Backend +- CI fails immediately when backend breaks the contract +- Tests interact with real backend, not mocks +- Validation errors are clear and actionable +- Tests stay simpler, no manual validation needed -### In Automated Testing (e.g., Playwright) +### Kubernetes Test Environments -This is one of the most powerful setups. +Deploy Mokapi as a sidecar or standalone validation layer in preview environments: +- Consistent contract validation for cluster traffic +- Early detection before staging deployment +- No modifications to backend services +- Integrates with Helm charts and GitOps workflows - Playwright → Mokapi → Backend +## Why Teams Choose This Approach -Benefits: -- CI fails immediately when the backend breaks the API contract -- tests interact with the real backend, not mocks -- validation errors are clear and actionable -- tests remain simpler — no need to validate everything in Playwright - -Your tests are guaranteed to hit a backend that actually matches the API contract. - -### In Kubernetes Test Environments - -Mokapi can also be used in temporary or preview environments to ensure contract validation across the entire cluster. - -In Kubernetes, Mokapi can be deployed as: -- a sidecar container -- a standalone validation layer in front of backend services -- a temporary component inside preview environments - -This brings: -- consistent contract validation for all cluster traffic -- early detection of breaking API changes before staging -- contract enforcement without modifying backend services -- transparent operation — apps talk to Mokapi, Mokapi talks to the backend - -You can integrate Mokapi into Helm charts, GitOps workflows, or test namespaces. - -## Why Teams Benefit from Using Mokapi Between Client and Backend - -### Automatic Contract Enforcement - -Every interaction is validated against your OpenAPI specification. Your backend can no longer quietly drift from the contract. +``` box=feature title="Automatic Contract Enforcement" +Every interaction is validated against your OpenAPI spec. Your backend can no longer silently drift from the contract. +``` -### Immediate Detection of Breaking Changes +``` box=feature title="Immediate Detection of Breaking Change" +Issues are caught early, renamed fields, wrong formats, unexpected status codes, mismatched data types before they reach production. +``` -Issues are caught early, not just in staging or production, such as: - - renamed or missing fields - - wrong or inconsistent formats - - unexpected status codes - - mismatched data types +``` box=feature title="More Reliable Frontend Development" +Frontend teams get consistent, validated API responses with fewer sudden breaking changes and a smoother development workflow. +``` -### More Reliable Frontend Development +``` box=feature title="Better Cross-Team Collaboration" +Backend developers instantly see contract violations. Frontend engineers get stable APIs. QA gets reliable test environments. Platform teams reduce deployment risk. +``` -Frontend teams get: -- consistent, validated API responses -- fewer sudden breaking changes -- a smoother development workflow +``` box=feature title="Smooth Mock-to-Real Transition" +Start with mocked endpoints in early development. Later, simply forward requests to the real backend while keeping validation in place. +``` -This reduces context-switching and debugging time. +``` box=feature title="Actionable Error Messages" +When validation fails, Mokapi provides clear, detailed error messages showing exactly what doesn't match the spec and why. +``` -### Better Collaboration Between Teams +> "Mokapi becomes your always-on API contract guardian, lightweight, transparent, and spec-driven." -With Mokapi validating both sides: -- backend developers instantly see when they violate the contract -- frontend engineers get stable, predictable APIs -- QA gets reliable test environments -- platform engineers reduce risk during deployments +## Getting Started -Mokapi becomes a shared API contract watchdog across the organization. +Implementing Mokapi as a validation layer is straightforward: -### Smooth Transition from Mocks to Real Systems +1. **Define your OpenAPI specification** for the API you want to validate +2. **Create a forwarding script** using the example above, mapping `request.api` to your backend URLs +3. **Run Mokapi** between your client and backend (locally, in Docker, or in Kubernetes) +4. **Point your client** at Mokapi instead of directly at the backend +5. **Watch validation errors surface** in real-time as you develop, test, and deploy -Teams often start with mocked endpoints in early development. Later, they can simply begin forwarding requests to the -real backend—while keeping validation in place. +No changes to your backend code. No infrastructure overhaul. Just a transparent validation layer +that enforces your API contract at runtime. -## Conclusion +## Stop API Drift. Start Enforcing Contracts. -Using Mokapi between frontend and backend, between backend services, or inside Kubernetes environments provides: -- strong contract enforcement -- automatic validation for every interaction -- early detection of breaking changes -- stable multi-team integration -- more reliable CI pipelines -- a smooth path from mocking to real backend validation +With Mokapi as a validation layer, you get automatic contract enforcement, early detection of breaking +changes, and stable multi-team integration, all without touching your backend code. -Mokapi ensures your API stays aligned with its specification, no matter how quickly your system evolves. +{{ cta-grid key="links" }} -> *Mokapi becomes your always-on API contract guardian — lightweight, transparent, and spec-driven.* \ No newline at end of file +

Ready to guard your API contracts? Start validating today.

\ No newline at end of file diff --git a/docs/resources/blogs/record-and-replay-api-interactions.md b/docs/resources/blogs/record-and-replay-api-interactions.md index 137efb217..f3367dbd2 100644 --- a/docs/resources/blogs/record-and-replay-api-interactions.md +++ b/docs/resources/blogs/record-and-replay-api-interactions.md @@ -1,14 +1,14 @@ --- title: "Record & Replay: API Interactions with Mokapi" -description: Capture real-world API traffic for testing, debugging, and offline development—all with a simple JavaScript script -subtitle: Capture real-world API traffic for testing, debugging, and offline development—all with a simple JavaScript script +description: Capture real-world API traffic for testing, debugging, and offline development, all with a simple JavaScript script +subtitle: Capture real-world API traffic for testing, debugging, and offline development, all with a simple JavaScript script tech: http tags: ['HTTP'] --- # Record & Replay: API Interactions with Mokapi -In a [previous article](ensuring-api-contract-compliance-with-mokapi.md), we explored how Mokapi can act as an API +In a [previous article](/resources/blogs/guard-your-api-contracts), we explored how Mokapi can act as an API specification guard between services—validating requests and responses against your OpenAPI specs in real-time. But Mokapi's capabilities go far beyond validation. In this article, we'll dive into another powerful use case: recording and replaying API interactions. diff --git a/docs/resources/tutorials/simple-http-api.md b/docs/resources/tutorials/simple-http-api.md index 60013531e..b78b53cc2 100644 --- a/docs/resources/tutorials/simple-http-api.md +++ b/docs/resources/tutorials/simple-http-api.md @@ -4,6 +4,7 @@ description: Learn how to mock a REST API using an OpenAPI specification with Mo subtitle: Learn how to mock a REST API using an OpenAPI specification with Mokapi. This tutorial covers basic setup, configuration, and Docker deployment. icon: bi-globe tech: http +tags: [HTTP, OpenAPI] cards: items: - title: Dynamic Responses @@ -15,8 +16,8 @@ cards: - title: CI/CD Integration href: /resources/blogs/end-to-end-testing-with-mocked-apis description: Run Mokapi in GitHub Actions for automated testing workflows - --- + # Get started with REST API In this tutorial, you will: diff --git a/docs/resources/tutorials/simple-kafka-api.md b/docs/resources/tutorials/simple-kafka-api.md index bb29c9d95..9b35f585d 100644 --- a/docs/resources/tutorials/simple-kafka-api.md +++ b/docs/resources/tutorials/simple-kafka-api.md @@ -1,19 +1,47 @@ --- title: Get started with Kafka description: Learn how to mock a Kafka Topic and verify that your producer generates valid messages according your AsyncAPI specification. +subtitle: Learn how to mock a Kafka topic using AsyncAPI specifications with Mokapi. Validate producer messages and test consumers without requiring a live Kafka cluster. +tags: [Kafka, AsyncAPI] icon: bi-lightning tech: kafka +cards: + items: + - title: Testing Kafka Workflows with Playwright and Mokapi + href: /resources/blogs/testing-kafka-workflows-playwright + description: Simulating real message flows end-to-end with Node.js, Kafka topics, and browser-driven tests. --- # Get started with Kafka -This tutorial provides a step-by-step guide to mocking a Kafka topic using Mokapi -and verifying that a producer generates valid messages based on an AsyncAPI specification. -This approach enables the simulation of Kafka environments without requiring a live Kafka cluster. +This tutorial provides a step-by-step guide to mocking a Kafka topic using Mokapi and verifying +that a producer generates valid messages based on an AsyncAPI specification. This approach enables +simulation of Kafka environments without requiring a live Kafka cluster. + +In this tutorial, you will: +- Create an AsyncAPI specification defining a Kafka topic with schema validation +- Configure and run Mokapi as a mock Kafka broker in Docker +- Write a .NET producer that sends messages to the mocked topic +- Verify message validation and monitoring using the Mokapi dashboard +- Create a .NET consumer to read messages from the mocked topic + +``` box=tree title="Project Structure" +📄 kafka.yaml +📄 Dockerfile +📁 Kafka.GetStarted (optional) + 📄 Consumer.cs + 📄 Kafka.GetStarted.csproj + 📄 Producer.cs + 📄 Program.cs +``` -## Create AsyncAPI file +``` box=info +You can find the [full working example](https://github.com/marle3003/mokapi/tree/main/examples/kafka/get-started) in the examples. +``` -Let's start by creating a file named `kafka.yml` with the following content. +## Create AsyncAPI file + +Create a file named `kafka.yaml` that defines a Kafka topic with message validation: ```yaml asyncapi: '2.0.0' @@ -69,17 +97,21 @@ components: - email ``` -This creates a Kafka topic `users` with two partitions. The content type of the message's payload -is `application/json` and the object must have the properties `id`, `name` and `email`. +### Specification Breakdown +- **Topic:** Creates a `users` topic with 2 partitions +- **Content Type:** Messages must be JSON (`application/json`) +- **Schema:** Each message must have `id` (UUID), `name` (string), and `email` (string) +- **Validation:** All three fields are required; missing fields will cause validation errors +- **Key Type:** Message keys must be strings -``` box=info -When Mokapi receives an invalid message it will return the error `CORRUPT_MESSAGE`and logs an -error, for instance `kafka: invalid message received for topic users: missing required field name...` +``` box=info title="Message Validation" +When Mokapi receives an invalid message, it will return the error CORRUPT_MESSAGE and log an error message +like: kafka: invalid message received for topic users: missing required field name... ``` -## Create a Dockerfile +## Create a Dockerfile -Next create a `Dockerfile` to configure Mokapi to use the AsyncAPI specification file +Create a `Dockerfile` to configure Mokapi with the AsyncAPI specification: ```dockerfile FROM mokapi/mokapi:latest @@ -89,17 +121,33 @@ COPY ./kafka.yaml /demo/ CMD ["--Providers.File.Directory=/demo"] ``` -## Start Mokapi +### Dockerfile Breakdown +- **Base Image:** Uses the official Mokapi Docker image +- **Copy Specification:** Copies the AsyncAPI specification into the container +- **Configuration:** Instructs Mokapi to load specifications from the `/demo` directory -Now we can start our container and verify in Mokapi's Dashboard (http://localhost:8080) our mocked Kafka topic +## Start Mokapi + +Build and run the Docker container, exposing the Kafka broker and dashboard ports: ``` docker run -p 9092:9092 -p 8080:8080 --rm -it $(docker build -q .) ``` -## Create a Kafka Producer +This command: +- **Builds** the Docker image from your Dockerfile +- **Exposes port 9092** for the Kafka broker protocol +- **Exposes port 8080** for the Mokapi dashboard +- **Runs interactively** with automatic cleanup when stopped + +``` box=result title="Mokapi is Running" +Open your browser and navigate to the Mokapi Dashboard at http://localhost:8080 to see your mocked Kafka +topic. You can verify the topic configuration, view messages, and monitor producer/consumer activity. +``` -You can now produce messages to the mocked Kafka topic. Below is an example using C#. +## Create a Kafka Producer + +Now you can produce messages to the mocked Kafka topic. Below is an example using C# with the Confluent.Kafka library: ```csharp public class Producer @@ -128,21 +176,40 @@ public class Producer } ``` -## Verifying Messages with Mokapi +### Producer Code Breakdown + +- **Connection:** Connects to the Mokapi broker at `localhost:9092` +- **Topic & Partition:** Targets the `users` topic, partition 1 +- **Message Key:** Uses `"alice"` as the message key (required by the AsyncAPI spec) +- **Message Value:** Serializes a JSON object with `id`, `name`, and `email` fields +- **JSON Format:** Uses camelCase naming to match the AsyncAPI schema + +``` box=info title="Schema Validation" +Mokapi validates every message against the AsyncAPI schema. If you send a message missing the name field or +with an invalid UUID format for id, Mokapi will reject it with a CORRUPT_MESSAGE error. +``` + +## Verifying Messages with Mokapi -Mokapi provides a dashboard to monitor and verify messages sent to the mocked Kafka topics. +Mokapi provides a comprehensive dashboard to monitor and verify messages sent to mocked Kafka topics. -1.

Access the Mokapi Dashboard:
Open your browser and navigate to http://localhost:8080/dashboard. +### Dashboard Verification Steps -2.

Navigate to the Kafka Section:
In the dashboard, select the Kafka section to view the topics and messages. +1. **Access the Mokapi Dashboard:** Open your browser and navigate to `http://localhost:8080/dashboard` +2. **Navigate to the Kafka Section:** In the dashboard, select the Kafka section to view configured topics and their messages +3. **Verify the Message:** Locate the `users` topic and verify that the message sent by your producer appears as expected. Mokapi displays the message key, value, partition, offset, and validation status -3.

Verify the Message:
Locate the users topic and verify that the message sent by your producer appears as expected. Mokapi will validate the message against the AsyncAPI specification. +![Mokapi Kafka Dashboard](/docs/resources/tutorials/simple-kafka-example.png "Mokapi Kafka Dashboard displaying validated messages in the users topic") -Mokapi Kafka Dashboard +The dashboard shows: +- **Message Content:** Full JSON payload with schema validation status +- **Partition & Offset:** Where the message was stored in the topic +- **Validation Errors:** Any schema mismatches or missing required fields +- **Timestamp:** When the message was received by Mokapi -## Create a Kafka Consumer +## Create a Kafka Consumer -Now we can use a .NET Consumer to consume our first sent message. +Now create a .NET consumer to read messages from the mocked Kafka topic: ```csharp public class Consumer @@ -168,4 +235,21 @@ public class Consumer } } } -``` \ No newline at end of file +``` + +### Consumer Code Breakdown +- **Connection:** Connects to the Mokapi broker at `localhost:9092` +- **Consumer Group:** Joins consumer group `"foo"` +- **Offset Reset:** Starts reading from the earliest available +- **Subscribe:** Subscribes to the `users` topic +- **Consume Loop:** Continuously polls for new messages and prints them to the console + +``` box=result title="Expected Console Output" +Consumed message '{"id":"dd5742d1-82ad-4d42-8960-cb21bd02f3e7","name":"Alice","email":"alice@foo.bar"}' offset: 0 partition: 1 +``` + +## Next Steps + +Now that you have a working Kafka mock with producer and consumer, explore these advanced topics: + +{{ card-grid key="cards" }} \ No newline at end of file diff --git a/docs/resources/tutorials/simple-kafka-example.png b/docs/resources/tutorials/simple-kafka-example.png index fbc8fed8f..1d993c68c 100644 Binary files a/docs/resources/tutorials/simple-kafka-example.png and b/docs/resources/tutorials/simple-kafka-example.png differ diff --git a/engine/mokapi_on_test.go b/engine/mokapi_on_test.go index 39b6b724b..ce4394f80 100644 --- a/engine/mokapi_on_test.go +++ b/engine/mokapi_on_test.go @@ -238,6 +238,30 @@ export default () => { test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) { require.NoError(t, err) + var res *common.HttpEventResponse + err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res) + require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data)) + }, + }, + { + name: "using Object.assign", + script: `import { on } from 'mokapi' +export default () => { + on('http', (req, res) => { + res.data = Object.assign(res.data, {foo: 'yuh'}) + }) +} +`, + run: func(evt common.EventEmitter) []*common.Action { + res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}} + actions := evt.Emit("http", &common.HttpEventRequest{}, res) + require.Nil(t, actions[0].Error) + require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data)) + return actions + }, + test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) { + require.NoError(t, err) + var res *common.HttpEventResponse err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res) require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data)) diff --git a/examples/kafka/get-started/Dockerfile b/examples/kafka/get-started/Dockerfile new file mode 100644 index 000000000..5fcbb8a67 --- /dev/null +++ b/examples/kafka/get-started/Dockerfile @@ -0,0 +1,5 @@ +FROM mokapi/mokapi:latest + +COPY ./kafka.yaml /demo/ + +CMD ["--Providers.File.Directory=/demo"] diff --git a/examples/kafka/get-started/Kafka.GetStarted/Consumer.cs b/examples/kafka/get-started/Kafka.GetStarted/Consumer.cs new file mode 100644 index 000000000..00db6db2f --- /dev/null +++ b/examples/kafka/get-started/Kafka.GetStarted/Consumer.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using Confluent.Kafka; + +public class Consumer +{ + public static void Run() + { + var config = new ConsumerConfig + { + BootstrapServers = "localhost:9092", + GroupId = "foo", + AutoOffsetReset = AutoOffsetReset.Earliest + }; + + using var consumer = new ConsumerBuilder(config).Build(); + consumer.Subscribe("users"); + + CancellationTokenSource cts = new CancellationTokenSource(); + + while (true) + { + var result = consumer.Consume(cts.Token); + Console.WriteLine($"Consumed message '{result.Message.Value}' offset: {result.TopicPartitionOffset.Offset} partition: {result.TopicPartition.Partition}"); + } + } +} diff --git a/examples/kafka/get-started/Kafka.GetStarted/Kafka.GetStarted.csproj b/examples/kafka/get-started/Kafka.GetStarted/Kafka.GetStarted.csproj new file mode 100644 index 000000000..e616ff77b --- /dev/null +++ b/examples/kafka/get-started/Kafka.GetStarted/Kafka.GetStarted.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/examples/kafka/get-started/Kafka.GetStarted/Producer.cs b/examples/kafka/get-started/Kafka.GetStarted/Producer.cs new file mode 100644 index 000000000..3bfc2c762 --- /dev/null +++ b/examples/kafka/get-started/Kafka.GetStarted/Producer.cs @@ -0,0 +1,29 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Confluent.Kafka; + +public class Producer +{ + public static async Task Run() + { + var config = new ProducerConfig { BootstrapServers = "localhost:9092" }; + + using var producer = new ProducerBuilder(config).Build(); + var topic = new TopicPartition("users", new Partition(1)); + + var serializeOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + var result = await producer.ProduceAsync(topic, new Message { + Key = "alice", + Value = JsonSerializer.Serialize(new { + Id = "dd5742d1-82ad-4d42-8960-cb21bd02f3e7", + Name = "Alice", + Email = "alice@foo.bar", + }, serializeOptions), + }); + } +} diff --git a/examples/kafka/get-started/Kafka.GetStarted/Program.cs b/examples/kafka/get-started/Kafka.GetStarted/Program.cs new file mode 100644 index 000000000..bb906b71d --- /dev/null +++ b/examples/kafka/get-started/Kafka.GetStarted/Program.cs @@ -0,0 +1,30 @@ +using System; + +if (args.Length == 0) +{ + PrintUsage(); + return; +} + +switch (args[0].ToLowerInvariant()) +{ + case "producer": + await Producer.Run(); + break; + + case "consumer": + Consumer.Run(); + break; + + default: + Console.WriteLine($"Unknown command: {args[0]}"); + PrintUsage(); + break; +} + +static void PrintUsage() +{ + Console.WriteLine("Usage:"); + Console.WriteLine(" dotnet run producer"); + Console.WriteLine(" dotnet run consumer"); +} diff --git a/examples/kafka/get-started/kafka.yaml b/examples/kafka/get-started/kafka.yaml new file mode 100644 index 000000000..fcc8a4637 --- /dev/null +++ b/examples/kafka/get-started/kafka.yaml @@ -0,0 +1,51 @@ +asyncapi: '2.0.0' +info: + title: Kafka Cluster + description: A kafka test cluster + version: '1.0' + contact: + name: Mokapi + url: https://mokapi.io + email: mokapi@mokapi.io +servers: + broker: + url: 127.0.0.1:9092 + protocol: kafka +channels: + users: + subscribe: + message: + $ref: '#/components/messages/user' + publish: + message: + $ref: '#/components/messages/user' + bindings: + kafka: + partitions: 2 + +components: + messages: + user: + contentType: application/json + payload: + $ref: '#/components/schemas/user' + bindings: + kafka: + key: + type: string + + schemas: + user: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + email: + type: string + required: + - id + - name + - email diff --git a/go.mod b/go.mod index ca5963e16..d085e918a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.5 require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/blevesearch/bleve/v2 v2.5.7 - github.com/blevesearch/bleve_index_api v1.3.1 + github.com/blevesearch/bleve_index_api v1.3.2 github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c diff --git a/go.sum b/go.sum index cdd76f89a..9af7b61d2 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= -github.com/blevesearch/bleve_index_api v1.3.1 h1:LdH3CQgBbIZ5UI/5Pykz87e0jfeQtVnrdZ2WUBrHHwU= -github.com/blevesearch/bleve_index_api v1.3.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/bleve_index_api v1.3.2 h1:y4VLXBF7nQR01CvF+QzmCJKMpVPCLp1CJ5FsRSZXzRE= +github.com/blevesearch/bleve_index_api v1.3.2/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= diff --git a/js/mokapi/on.go b/js/mokapi/on.go index 2fb2407ce..7eeee3728 100644 --- a/js/mokapi/on.go +++ b/js/mokapi/on.go @@ -15,7 +15,7 @@ import ( type onArgs struct { tags map[string]string - track bool + track func(args ...goja.Value) (bool, error) isTrackSet bool priority int } @@ -32,13 +32,14 @@ func (m *Module) On(event string, do goja.Value, vArgs goja.Value) { return false, err } + var params []goja.Value + for _, v := range ctx.Args { + params = append(params, ArgToJs(v, m.vm)) + } + var r goja.Value r, err = m.loop.RunAsync(func(vm *goja.Runtime) (goja.Value, error) { call, _ := goja.AssertFunction(do) - var params []goja.Value - for _, v := range ctx.Args { - params = append(params, ArgToJs(v, m.vm)) - } v, err := call(goja.Undefined(), params...) if err != nil { return nil, err @@ -55,7 +56,7 @@ func (m *Module) On(event string, do goja.Value, vArgs goja.Value) { } if eventArgs.isTrackSet { - return eventArgs.track, nil + return eventArgs.track(params...) } newHashes, err := getHashes(ctx.Args...) @@ -96,11 +97,26 @@ func getOnArgs(vm *goja.Runtime, args goja.Value) (onArgs, error) { if goja.IsUndefined(v) || goja.IsNull(v) { continue } - if v.ExportType().Kind() != reflect.Bool { + if v.ExportType().Kind() == reflect.Bool { + result.isTrackSet = true + result.track = func(args ...goja.Value) (bool, error) { + return v.ToBoolean(), nil + } + } else if f, ok := goja.AssertFunction(v); ok { + result.isTrackSet = true + result.track = func(args ...goja.Value) (bool, error) { + r, err := f(goja.Undefined(), args...) + if err != nil { + return true, fmt.Errorf("failed to call track function: %v", err) + } + if r.ExportType().Kind() == reflect.Bool { + return r.ToBoolean(), nil + } + return true, fmt.Errorf("unexpected return type for track: %v", util.JsType(r.Export())) + } + } else { return onArgs{}, fmt.Errorf("unexpected type for track: %v", util.JsType(v.Export())) } - result.track = v.ToBoolean() - result.isTrackSet = true case "priority": v := params.Get(k) if goja.IsUndefined(v) || goja.IsNull(v) { diff --git a/js/mokapi/on_test.go b/js/mokapi/on_test.go index c61560a61..67719a382 100644 --- a/js/mokapi/on_test.go +++ b/js/mokapi/on_test.go @@ -138,6 +138,99 @@ func TestModule_On(t *testing.T) { r.Equal(t, false, b) }, }, + { + name: "dynamic track", + test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) { + var handler common.EventHandler + host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) { + handler = do + } + + _, err := vm.RunString(` + const m = require('mokapi') + let nextTrack = true + m.on('http', (param) => { param['foo'] = false }, + { + track: () => { + const track = nextTrack + nextTrack = false + return track + } + } + ) + `) + r.NoError(t, err) + + // first call + b, err := handler(&common.EventContext{Args: []any{map[string]bool{"foo": true}}}) + r.NoError(t, err) + r.True(t, b) + + // second call + b, err = handler(&common.EventContext{Args: []any{map[string]bool{"foo": true}}}) + r.NoError(t, err) + r.False(t, b) + }, + }, + { + name: "dynamic track using parameters", + test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) { + var handler common.EventHandler + host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) { + handler = do + } + + _, err := vm.RunString(` + const m = require('mokapi') + let nextTrack = true + m.on('http', (param) => { param['foo'] = !param['foo'] }, + { + track: (param) => { + return param['foo'] + } + } + ) + `) + r.NoError(t, err) + + // first call + b, err := handler(&common.EventContext{Args: []any{map[string]bool{"foo": false}}}) + r.NoError(t, err) + r.True(t, b) + + // second call + b, err = handler(&common.EventContext{Args: []any{map[string]bool{"foo": true}}}) + r.NoError(t, err) + r.False(t, b) + }, + }, + { + name: "dynamic track returning wrong type", + test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) { + var handler common.EventHandler + host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) { + handler = do + } + + _, err := vm.RunString(` + const m = require('mokapi') + let nextTrack = true + m.on('http', (param) => { param['foo'] = !param['foo'] }, + { + track: (param) => { + return 123 + } + } + ) + `) + r.NoError(t, err) + + // first call + b, err := handler(&common.EventContext{Args: []any{map[string]bool{"foo": false}}}) + r.EqualError(t, err, "unexpected return type for track: Integer") + r.True(t, b) + }, + }, { name: "event handler throws error", test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) { diff --git a/js/mokapi/proxy.go b/js/mokapi/proxy.go index 065ca3bd9..bb1acfe85 100644 --- a/js/mokapi/proxy.go +++ b/js/mokapi/proxy.go @@ -308,7 +308,11 @@ func assignValue(field reflect.Value, value any, fieldName string) error { field.Set(reflect.Zero(field.Type())) return nil } - v, err := convertTo(field.Type(), reflect.ValueOf(value)) + v := reflect.ValueOf(value) + if p, ok := value.(*Proxy); ok { + v = p.target + } + v, err := convertTo(field.Type(), v) if err != nil { return fmt.Errorf("failed to set %s: %w", fieldName, err) } diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts index bcbff1719..a0dc7912d 100644 --- a/npm/types/index.d.ts +++ b/npm/types/index.d.ts @@ -1,6 +1,12 @@ /** * Mokapi JavaScript API - * https://mokapi.io/docs/welcome + * + * This module exposes the core scripting API for Mokapi. + * It allows you to intercept and manipulate protocol events (HTTP, Kafka, LDAP, SMTP), + * schedule jobs, generate mock data, and share state between scripts. + * + * Documentation: + * https://mokapi.io/docs/javascript-api/overview */ import "./faker"; @@ -14,10 +20,14 @@ import "./file" /** * Attaches an event handler for the given event. + * + * Event handlers are executed in priority order whenever the event occurs. + * Multiple handlers can be registered for the same event. + * * https://mokapi.io/docs/javascript-api/mokapi/on - * @param event Event type such as http - * @param handler An EventHandler to execute when the event is triggered - * @param args EventArgs object contains additional event arguments. + * @param event Event type such as `http`, `kafka`, `ldap`, or `smtp` + * @param handler Function executed when the event is triggered + * @param args Optional event configuration such as priority, tracking, or tags * @example * export default function() { * on('http', function(request, response) { @@ -30,7 +40,7 @@ import "./file" * }) * } */ -export function on(event: T, handler: EventHandler[T], args?: EventArgs): void; +export function on(event: T, handler: EventHandler[T], args?: TypedEventArgs[T]): void; /** * Schedules a new periodic job with interval. @@ -110,16 +120,18 @@ export interface EventHandler { } /** - * HttpEventHandler is a function that is executed when an HTTP event is triggered. + * HttpEventHandler is invoked for every incoming HTTP request. + * + * Handlers may modify the response object to influence the outgoing response. + * The return value is ignored. + * * https://mokapi.io/docs/javascript-api/mokapi/eventhandler/httpeventhandler * @example * export default function() { * on('http', function(request, response) { * if (request.operationId === 'time') { * response.body = date() - * return true * } - * return false * }) * } */ @@ -189,20 +201,28 @@ export interface HttpResponse { data: any; /** - * Rebuilds the entire HTTP response using the OpenAPI response definition for the given status code and content type - * @example - * import { on } from 'mokapi' + * Rebuilds the entire HTTP response using the OpenAPI response definition. + * + * This resets the status code, headers, and response body/data + * based on the OpenAPI specification. + * + * - If `statusCode` is omitted, the OpenAPI `default` response is used. + * - If `contentType` is omitted, the first defined content type for the + * selected status code is used. + * + * Use this when switching to a different response (e.g. error handling) + * while keeping the response schema valid. + * + * @throws Error if the status code or content type is not defined in the OpenAPI spec * - * export default function() { - * on('http', (request, response) => { - * if (request.path.petId === 10) { - * // Switch to a different OpenAPI response. - * response.rebuild(404, 'application/json') - * response.data.message = 'Pet not found' - * } - * }) - * } - * */ + * @example + * on('http', (request, response) => { + * if (request.path.petId === 10) { + * response.rebuild(404) + * response.data.message = 'Pet not found' + * } + * }) + */ rebuild: (statusCode?: number, contentType?: string) => void; } @@ -465,6 +485,55 @@ export interface EventArgs { */ tags?: { [key: string]: string }; + /** + * Defines the execution order of the event handler. + * + * Event handlers are executed in descending priority order. + * Handlers with the same priority are executed in registration order. + * + * Handlers with higher priority values run first. + * Handlers with lower priority values run later. + * + * Use negative priorities (e.g. -1) to run a handler after + * the response has been fully populated by other handlers, + * such as for logging or recording purposes. + */ + priority?: number; +} + +/** + * TypedEventArgs provides strongly typed argument objects + * for each supported event type. + * + * It is mainly used internally to map event names + * (e.g. `http`, `kafka`) to their corresponding argument types. + */ +export interface TypedEventArgs { + /** + * Arguments for HTTP event handlers. + */ + http: HttpEventArgs; + /** + * Arguments for Kafka event handlers. + */ + kafka: KafkaEventArgs; + /** + * Arguments for LDAP event handlers. + */ + ldap: LdapEventArgs; + /** + * Arguments for SMTP event handlers. + */ + smtp: SmtpEventArgs; +} + +/** + * Configuration options for HTTP event handlers. + * + * These arguments control execution behavior such as + * priority, tagging, and dashboard tracking. + */ +export interface HttpEventArgs extends EventArgs { /** * Controls whether this event handler is tracked in the dashboard. * @@ -473,19 +542,61 @@ export interface EventArgs { * - undefined: Mokapi determines tracking automatically based on * whether the response object was modified by the handler */ - track?: boolean; + track?: boolean | ((request: HttpRequest, response: HttpResponse) => boolean); +} +/** + * Configuration options for Kafka event handlers. + * + * These arguments control execution behavior such as + * priority, tagging, and dashboard tracking. + */ +export interface KafkaEventArgs extends EventArgs { /** - * Defines the execution order of the event handler. + * Controls whether this event handler is tracked in the dashboard. * - * Handlers with higher priority values run first. - * Handlers with lower priority values run later. + * - true: always track this handler + * - false: never track this handler + * - undefined: Mokapi determines tracking automatically based on + * whether the message was modified or acknowledged by the handler + */ + track?: boolean | ((message: KafkaEventMessage) => boolean); +} + +/** + * Configuration options for LDAP event handlers. + * + * These arguments control execution behavior such as + * priority, tagging, and dashboard tracking. + */ +export interface LdapEventArgs extends EventArgs { + /** + * Controls whether this event handler is tracked in the dashboard. * - * Use negative priorities (e.g. -1) to run a handler after - * the response has been fully populated by other handlers, - * such as for logging or recording purposes. + * - true: always track this handler + * - false: never track this handler + * - undefined: Mokapi determines tracking automatically based on + * whether the response object was modified by the handler */ - priority?: number; + track?: boolean | ((request: LdapSearchRequest, response: LdapSearchResponse) => boolean); +} + +/** + * Configuration options for SMTP event handlers. + * + * These arguments control execution behavior such as + * priority, tagging, and dashboard tracking. + */ +export interface SmtpEventArgs extends EventArgs { + /** + * Controls whether this event handler is tracked in the dashboard. + * + * - true: always track this handler + * - false: never track this handler + * - undefined: Mokapi determines tracking automatically based on + * whether the message was processed or modified by the handler + */ + track?: boolean | ((record: SmtpEventMessage) => boolean); } /** @@ -500,6 +611,10 @@ export interface EventArgs { */ export type ScheduledEventHandler = () => void | Promise; +/** +* Configuration options for scheduled event handlers +* created via `every` or `cron`. +*/ export interface ScheduledEventArgs { /** * Adds or overrides existing tags used in dashboard @@ -663,7 +778,8 @@ export interface SharedMemory { * The `mokapi.shared` object provides a way to persist and share * data between multiple scripts running in the same Mokapi instance. * - * Values are stored in memory and shared across all scripts. + * Values are stored in memory and shared across all scripts + * within the same Mokapi process. * This allows you to coordinate state, cache data, or simulate * application-level variables without using global variables. * All values are persisted for the lifetime of the Mokapi process. diff --git a/pkg/cmd/mokapi/flags/api.go b/pkg/cmd/mokapi/flags/api.go index 9bb12ae49..b1177ec66 100644 --- a/pkg/cmd/mokapi/flags/api.go +++ b/pkg/cmd/mokapi/flags/api.go @@ -8,6 +8,8 @@ func RegisterApiFlags(cmd *cli.Command) { cmd.Flags().String("api-base", "", apiBase) cmd.Flags().Bool("api-dashboard", true, apiDashboard) cmd.Flags().Bool("api-search-enabled", true, apiSearch) + cmd.Flags().String("api-search-index-path", "", apiSearchIndexPath) + cmd.Flags().Bool("api-search-in-memory", false, apiSearchInMemory) } var apiPort = cli.FlagDoc{ @@ -84,3 +86,63 @@ When enabled, users can search through mocked APIs, resources, and requests dire }, }, } + +var apiSearchIndexPath = cli.FlagDoc{ + Short: "Set storage location for the dashboard search index", + Long: `Defines where Mokapi stores the search index used by the web dashboard. + +By default, Mokapi stores the search index in a temporary directory on disk. +This reduces memory usage and provides predictable memory behavior. + +If a file system path is specified, the index will be stored at that location during Mokapi's runtime. +The index is rebuilt at startup and does not block application startup.`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + { + Title: "CLI", + Source: "--api-search-index-path /var/lib/mokapi/search", + }, + { + Title: "Env", + Source: "MOKAPI_API_SEARCH_INDEX_PATH=/var/lib/mokapi/search", + }, + { + Title: "File", + Source: `api: + search: + indexPath: /var/lib/mokapi/search`, + }, + }, + }, + }, +} + +var apiSearchInMemory = cli.FlagDoc{ + Short: "Store the dashboard search index entirely in memory", + Long: `Forces Mokapi to keep the search index entirely in memory instead of using disk storage. + +This provides fast indexing and search performance but increases Go heap memory usage. + +This option is recommended only for small projects or development environments.`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + { + Title: "CLI", + Source: "--api-search-in-memory", + }, + { + Title: "Env", + Source: "MOKAPI_API_SEARCH_IN_MEMORY=true", + }, + { + Title: "File", + Source: `api: + search: + inMemory: true`, + }, + }, + }, + }, +} diff --git a/pkg/cmd/mokapi/flags/api_test.go b/pkg/cmd/mokapi/flags/api_test.go new file mode 100644 index 000000000..7d5c7a90d --- /dev/null +++ b/pkg/cmd/mokapi/flags/api_test.go @@ -0,0 +1,60 @@ +package flags_test + +import ( + "mokapi/config/static" + "mokapi/pkg/cli" + "mokapi/pkg/cmd/mokapi" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoot_Api(t *testing.T) { + testcases := []struct { + name string + cmd func(t *testing.T) *cli.Command + test func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) + }{ + { + name: "search index path", + cmd: func(t *testing.T) *cli.Command { + cmd := mokapi.NewCmdMokapi() + cmd.SetArgs([]string{"--api-search-index-path", "/tmp"}) + return cmd + }, + test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) { + require.Equal(t, "/tmp", cfg.Api.Search.IndexPath) + }, + }, + { + name: "search in memory", + cmd: func(t *testing.T) *cli.Command { + cmd := mokapi.NewCmdMokapi() + cmd.SetArgs([]string{"--api-search-in-memory"}) + return cmd + }, + test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) { + require.Equal(t, true, cfg.Api.Search.InMemory) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + cli.SetFileReader(&cli.FileReader{}) + }() + + cmd := tc.cmd(t) + var cfg *static.Config + cmd.Run = func(cmd *cli.Command, args []string) error { + cfg = cmd.Config.(*static.Config) + return nil + } + err := cmd.Execute() + require.NoError(t, err) + + tc.test(t, cfg, cmd.Flags()) + }) + } +} diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index ac305e582..e14a37bbc 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -136,6 +136,15 @@ func createServer(cfg *static.Config) (*server.Server, error) { return nil, err } http := server.NewHttpManager(scriptEngine, certStore, app) + if u, err := api.BuildUrl(cfg.Api); err == nil { + err = http.AddInternalService("api", u, api.New(app, cfg.Api)) + if err != nil { + return nil, err + } + } else { + return nil, err + } + kafka := server.NewKafkaManager(scriptEngine, app) mqtt := server.NewMqttManager(scriptEngine, app) mailManager := server.NewMailManager(app, scriptEngine, certStore) @@ -153,15 +162,6 @@ func createServer(cfg *static.Config) (*server.Server, error) { app.UpdateConfig(e) }) - if u, err := api.BuildUrl(cfg.Api); err == nil { - err = http.AddInternalService("api", u, api.New(app, cfg.Api)) - if err != nil { - return nil, err - } - } else { - return nil, err - } - return server.NewServer(pool, app, watcher, kafka, http, mailManager, ldap, scriptEngine), nil } diff --git a/providers/openapi/handler_requestbody_test.go b/providers/openapi/handler_requestbody_test.go index fab206c00..04a1ece39 100644 --- a/providers/openapi/handler_requestbody_test.go +++ b/providers/openapi/handler_requestbody_test.go @@ -14,7 +14,6 @@ import ( "strings" "testing" - "github.com/blevesearch/bleve/v2" "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" ) @@ -211,9 +210,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) { return nil }) - idx, err := bleve.NewMemOnly(bleve.NewIndexMapping()) - require.NoError(t, err) - store := events.NewStoreManager(idx) + store := events.NewStoreManager(&index{}) store.SetStore(10, events.NewTraits().WithNamespace("http")) tc.fn(t, openapi.NewHandler(tc.config, e, store)) @@ -236,3 +233,9 @@ func (s *spyBody) Read(p []byte) (n int, err error) { func (s *spyBody) Close() error { return nil } + +type index struct{} + +func (*index) Add(string, any) {} + +func (*index) Delete(string) {} diff --git a/providers/openapi/handler_test.go b/providers/openapi/handler_test.go index 4fe56371a..94404738b 100644 --- a/providers/openapi/handler_test.go +++ b/providers/openapi/handler_test.go @@ -1162,7 +1162,7 @@ func TestHandler_Event(t *testing.T) { Servers: []*openapi.Server{{Url: "http://localhost"}}, Components: openapi.Components{}, } - sm := &events.StoreManager{} + sm := events.NewStoreManager(&index{}) sm.SetStore(1, events.NewTraits().WithNamespace("http")) eh := &engine{emit: tc.event} @@ -1572,7 +1572,7 @@ func TestHandler_Parameter(t *testing.T) { } tc.test(t, func(rw http.ResponseWriter, r *http.Request) { - h := openapi.NewHandler(config, &engine{emit: tc.event}, &events.StoreManager{}) + h := openapi.NewHandler(config, &engine{emit: tc.event}, events.NewStoreManager(&index{})) _ = h.ServeHTTP(rw, r) }, config) }) diff --git a/providers/openapi/parameter_cookie.go b/providers/openapi/parameter_cookie.go index f56a04e7f..bae6609d0 100644 --- a/providers/openapi/parameter_cookie.go +++ b/providers/openapi/parameter_cookie.go @@ -1,7 +1,6 @@ package openapi import ( - "errors" "fmt" "mokapi/providers/openapi/schema" "net/http" @@ -10,11 +9,14 @@ import ( func parseCookie(param *Parameter, r *http.Request) (*RequestParameterValue, error) { cookie, err := r.Cookie(param.Name) - if err != nil || (len(cookie.Value) == 0 && param.Required) { - if errors.Is(err, http.ErrNoCookie) && !param.Required { - return nil, nil + if err != nil || len(cookie.Value) == 0 { + if param.Required { + return nil, fmt.Errorf("parameter is required") } - return nil, fmt.Errorf("parameter is required") + if param.Schema != nil && param.Schema.Default != nil { + return &RequestParameterValue{Value: param.Schema.Default}, nil + } + return nil, nil } rp := &RequestParameterValue{Raw: &(cookie.Value), Value: cookie.Value} diff --git a/providers/openapi/parameter_cookie_test.go b/providers/openapi/parameter_cookie_test.go index 2f670023d..fa8439a8d 100644 --- a/providers/openapi/parameter_cookie_test.go +++ b/providers/openapi/parameter_cookie_test.go @@ -1,12 +1,13 @@ package openapi_test import ( - "github.com/stretchr/testify/require" "mokapi/providers/openapi" "mokapi/providers/openapi/schema/schematest" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/require" ) func TestFromRequest_Cookie(t *testing.T) { @@ -112,6 +113,22 @@ func TestFromRequest_Cookie(t *testing.T) { require.EqualError(t, err, "parse cookie parameter 'debug' failed: parameter is required") }, }, + { + name: "cookie with default", + params: openapi.Parameters{{Value: &openapi.Parameter{ + Type: openapi.ParameterCookie, + Name: "debug", + Schema: schematest.New("integer", schematest.WithDefault(10)), + }}}, + request: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "https://foo.bar", nil) + return r + }, + test: func(t *testing.T, result *openapi.RequestParameters, err error) { + require.NoError(t, err) + require.Equal(t, 10, result.Cookie["debug"].Value) + }, + }, { name: "invalid value", params: openapi.Parameters{{Value: &openapi.Parameter{ diff --git a/providers/openapi/parameter_header.go b/providers/openapi/parameter_header.go index c5ab2d7e1..2f27b824e 100644 --- a/providers/openapi/parameter_header.go +++ b/providers/openapi/parameter_header.go @@ -14,6 +14,9 @@ func parseHeader(param *Parameter, r *http.Request) (*RequestParameterValue, err if param.Required { return nil, fmt.Errorf("parameter is required") } + if param.Schema != nil && param.Schema.Default != nil { + return &RequestParameterValue{Value: param.Schema.Default}, nil + } return nil, nil } diff --git a/providers/openapi/parameter_header_test.go b/providers/openapi/parameter_header_test.go index 2d978bc4b..9ea06ed0c 100644 --- a/providers/openapi/parameter_header_test.go +++ b/providers/openapi/parameter_header_test.go @@ -73,6 +73,23 @@ func TestFromRequest_Header(t *testing.T) { require.Equal(t, "1", *cookie.Raw) }, }, + { + name: "with default", + params: openapi.Parameters{{Value: &openapi.Parameter{ + Type: openapi.ParameterHeader, + Name: "debug", + Required: false, + Schema: schematest.New("integer", schematest.WithDefault(10)), + }}}, + request: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "https://foo.bar", nil) + return r + }, + test: func(t *testing.T, result *openapi.RequestParameters, err error) { + require.NoError(t, err) + require.Equal(t, 10, result.Header["debug"].Value) + }, + }, { name: "not required header and not sent", params: openapi.Parameters{{Value: &openapi.Parameter{ diff --git a/providers/openapi/parameter_http_test.go b/providers/openapi/parameter_http_test.go index b2140e341..1beec12b4 100644 --- a/providers/openapi/parameter_http_test.go +++ b/providers/openapi/parameter_http_test.go @@ -1,11 +1,12 @@ package openapi import ( - "github.com/stretchr/testify/require" "mokapi/providers/openapi/schema/schematest" "net/http" "net/url" "testing" + + "github.com/stretchr/testify/require" ) func TestParseParam(t *testing.T) { diff --git a/providers/openapi/parameter_query.go b/providers/openapi/parameter_query.go index 4baa56fb3..41aec7182 100644 --- a/providers/openapi/parameter_query.go +++ b/providers/openapi/parameter_query.go @@ -27,7 +27,11 @@ func parseQuery(param *Parameter, u *url.URL) (*RequestParameterValue, error) { if param.Required { return nil, fmt.Errorf("parameter is required") } - return &RequestParameterValue{}, err + v := &RequestParameterValue{} + if param.Schema != nil && param.Schema.Default != nil { + v.Value = param.Schema.Default + } + return v, err } raw := u.Query().Get(param.Name) rp := &RequestParameterValue{Raw: &raw} @@ -46,15 +50,21 @@ func parseQuery(param *Parameter, u *url.URL) (*RequestParameterValue, error) { func parseQueryObject(param *Parameter, u *url.URL) (string, interface{}, error) { if param.Style == "form" && param.IsExplode() { raw := u.RawQuery - if len(raw) == 0 && param.Required { - return "", nil, fmt.Errorf("parameter is required") + if len(raw) == 0 { + if param.Required { + return "", nil, fmt.Errorf("parameter is required") + } + return "", param.Schema.Default, nil } i, err := parseExplodeObject(param, raw, "&", url.QueryUnescape) return raw, i, err } else if param.Style == "form" { raw := u.Query().Get(param.Name) - if len(raw) == 0 && param.Required { - return "", nil, fmt.Errorf("parameter is required") + if len(raw) == 0 { + if param.Required { + return "", nil, fmt.Errorf("parameter is required") + } + return "", param.Schema.Default, nil } i, err := parseUnExplodeObject(param, raw, ",") return raw, i, err @@ -78,8 +88,11 @@ func parseQueryObject(param *Parameter, u *url.URL) (string, interface{}, error) obj[name] = v } } - if len(raw.String()) == 0 && param.Required { - return "", nil, fmt.Errorf("parameter is required") + if len(raw.String()) == 0 { + if param.Required { + return "", nil, fmt.Errorf("parameter is required") + } + return "", param.Schema.Default, nil } return raw.String(), obj, nil @@ -106,8 +119,11 @@ func parseQueryArray(p *Parameter, u *url.URL) (*string, any, error) { values = strings.Split(raw, ",") } } - + i, err := parseArray(p, values) + return &raw, i, err + } + if p.Schema != nil && p.Schema.Default != nil { + return nil, p.Schema.Default, nil } - i, err := parseArray(p, values) - return &raw, i, err + return &raw, nil, nil } diff --git a/providers/openapi/parameter_query_test.go b/providers/openapi/parameter_query_test.go index d8781b712..36d9bbaac 100644 --- a/providers/openapi/parameter_query_test.go +++ b/providers/openapi/parameter_query_test.go @@ -93,6 +93,27 @@ func TestParseQuery(t *testing.T) { require.EqualError(t, err, "parse query parameter 'id' failed: parameter is required") }, }, + { + name: "no query parameter with default", + params: openapi.Parameters{ + {Value: &openapi.Parameter{ + Name: "id", + Type: openapi.ParameterQuery, + Schema: schematest.New("integer", schematest.WithDefault(10)), + Style: "form", + Explode: explode(false), + }}, + }, + request: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "https://foo.bar", nil) + }, + test: func(t *testing.T, result *openapi.RequestParameters, err error) { + require.NoError(t, err) + require.Len(t, result.Query, 1) + require.Equal(t, 10, result.Query["id"].Value) + require.Nil(t, result.Query["id"].Raw) + }, + }, { name: "integer array as form and explode", params: openapi.Parameters{ @@ -208,6 +229,25 @@ func TestParseQuery(t *testing.T) { require.Equal(t, []interface{}{int64(3), int64(4), int64(5)}, result.Query["id"].Value) }, }, + { + name: "array with default", + params: openapi.Parameters{ + {Value: &openapi.Parameter{ + Name: "id", + Type: openapi.ParameterQuery, + Schema: schematest.New("array", schematest.WithDefault([]any{1, 2, 3})), + Style: "pipeDelimited", + Explode: explode(false), + }}, + }, + request: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "https://foo.bar", nil) + }, + test: func(t *testing.T, result *openapi.RequestParameters, err error) { + require.NoError(t, err) + require.Equal(t, []any{1, 2, 3}, result.Query["id"].Value) + }, + }, { name: "object explode", params: openapi.Parameters{ @@ -447,6 +487,27 @@ func TestParseQuery(t *testing.T) { require.EqualError(t, err, "parse query parameter 'id' failed: parameter is required") }, }, + { + name: "object with default", + params: openapi.Parameters{ + {Value: &openapi.Parameter{ + Name: "id", + Type: openapi.ParameterQuery, + Schema: schematest.New("object", + schematest.WithProperty("role", schematest.New("string")), + schematest.WithProperty("firstName", schematest.New("string")), + schematest.WithDefault(map[string]any{"role": "admin", "firstName": "Alex"}), + ), + Style: "form", + }}, + }, + request: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "https://foo.bar", nil) + }, + test: func(t *testing.T, result *openapi.RequestParameters, err error) { + require.Equal(t, map[string]any{"role": "admin", "firstName": "Alex"}, result.Query["id"].Value) + }, + }, { name: "boolean value true", params: openapi.Parameters{ diff --git a/providers/swagger/config.go b/providers/swagger/config.go index 050e0a590..4d244078c 100644 --- a/providers/swagger/config.go +++ b/providers/swagger/config.go @@ -95,9 +95,9 @@ type Parameter struct { MultipleOf *float64 `yaml:"multipleOf,omitempty" json:"multipleOf,omitempty"` Minimum *float64 `yaml:"minimum,omitempty" json:"minimum,omitempty"` Maximum *float64 `yaml:"maximum,omitempty" json:"maximum,omitempty"` - MaxLength *uint64 `yaml:"maxLength,omitempty" json:"maxLength,omitempty"` + MaxLength *int `yaml:"maxLength,omitempty" json:"maxLength,omitempty"` MaxItems *int `yaml:"maxItems,omitempty" json:"maxItems,omitempty"` - MinLength int64 `yaml:"minLength,omitempty" json:"minLength,omitempty"` + MinLength *int `yaml:"minLength,omitempty" json:"minLength,omitempty"` MinItems int `yaml:"minItems,omitempty" json:"minItems,omitempty"` Default interface{} `yaml:"default,omitempty" json:"default,omitempty"` } diff --git a/providers/swagger/convert.go b/providers/swagger/convert.go index ab9c3dc99..2a9aef143 100644 --- a/providers/swagger/convert.go +++ b/providers/swagger/convert.go @@ -278,13 +278,18 @@ func convertParameter(p *Parameter) *openapi.ParameterRef { Format: p.Format, Pattern: p.Pattern, Items: p.Items, + Default: p.Default, Minimum: p.Minimum, Maximum: p.Maximum, ExclusiveMinimum: jsonSchema.NewUnionTypeB[float64, bool](p.ExclusiveMin), ExclusiveMaximum: jsonSchema.NewUnionTypeB[float64, bool](p.ExclusiveMin), + MaxLength: p.MaxLength, + MinLength: p.MinLength, UniqueItems: p.UniqueItems, MinItems: &p.MinItems, MaxItems: p.MaxItems, + Enum: p.Enum, + MultipleOf: p.MultipleOf, }, Required: p.Required, Deprecated: p.Deprecated, diff --git a/providers/swagger/convert_test.go b/providers/swagger/convert_test.go index c16e15354..eb35b839a 100644 --- a/providers/swagger/convert_test.go +++ b/providers/swagger/convert_test.go @@ -325,6 +325,26 @@ func TestConvert(t *testing.T) { require.Equal(t, "header", get.Parameters[0].Value.Type.String()) }, }, + { + name: "operation parameter type", + config: `{"swagger": "2.0","paths": {"/foo": {"get": {"parameters": [{"in":"header","name":"id","type":"string"}]}}}}`, + test: func(t *testing.T, config *openapi.Config) { + require.Contains(t, config.Paths, "/foo") + get := config.Paths["/foo"].Value.Get + require.Equal(t, "header", get.Parameters[0].Value.Type.String()) + require.Equal(t, "string", get.Parameters[0].Value.Schema.Type.String()) + }, + }, + { + name: "operation parameter default value", + config: `{"swagger": "2.0","paths": {"/foo": {"get": {"parameters": [{"in":"header","name":"id","default":10}]}}}}`, + test: func(t *testing.T, config *openapi.Config) { + require.Contains(t, config.Paths, "/foo") + get := config.Paths["/foo"].Value.Get + require.Equal(t, "header", get.Parameters[0].Value.Type.String()) + require.Equal(t, float64(10), get.Parameters[0].Value.Schema.Default) + }, + }, { name: "operation default response", config: `{"swagger": "2.0", "paths": {"/foo": {"get": {"responses": { "default": { "description": "default" } }}}}}`, diff --git a/runtime/events/events.go b/runtime/events/events.go index 03d9553a7..60a293067 100644 --- a/runtime/events/events.go +++ b/runtime/events/events.go @@ -2,11 +2,11 @@ package events import ( "fmt" + "mokapi/runtime/search" "sort" "sync" "time" - "github.com/blevesearch/bleve/v2" "github.com/google/uuid" log "github.com/sirupsen/logrus" ) @@ -22,11 +22,11 @@ type EventData interface { type StoreManager struct { stores []*store - index bleve.Index + index search.Index m sync.RWMutex } -func NewStoreManager(index bleve.Index) *StoreManager { +func NewStoreManager(index search.Index) *StoreManager { return &StoreManager{index: index} } @@ -56,7 +56,7 @@ func (m *StoreManager) Push(data EventData, traits Traits) error { removed := bestStore.Push(evt) if removed != nil { - m.removeFromIndex(removed) + m.index.Delete(removed.Id) } return nil diff --git a/runtime/events/events_test.go b/runtime/events/events_test.go index a8fbc27cd..1aad07302 100644 --- a/runtime/events/events_test.go +++ b/runtime/events/events_test.go @@ -145,7 +145,13 @@ func TestPush(t *testing.T) { for _, tc := range testcase { tc := tc t.Run(tc.name, func(t *testing.T) { - tc.f(t, &events.StoreManager{}) + tc.f(t, events.NewStoreManager(&index{})) }) } } + +type index struct{} + +func (*index) Add(string, any) {} + +func (*index) Delete(string) {} diff --git a/runtime/events/index.go b/runtime/events/index.go index ba0d7a157..13de8e724 100644 --- a/runtime/events/index.go +++ b/runtime/events/index.go @@ -2,7 +2,6 @@ package events import ( "fmt" - log "github.com/sirupsen/logrus" "mokapi/runtime/search" "reflect" "time" @@ -27,20 +26,13 @@ func (m *StoreManager) addToIndex(event *Event) { Discriminator: fmt.Sprintf("event_%s", event.Traits.String()), Api: getApiFromEvent(event), Event: event, - Title: event.Data.Title(), Time: event.Time.Format(time.RFC3339), } - - if err := m.index.Index(event.Id, data); err != nil { - log.Errorf("add '%s' to search index failed: %v", event.Id, err) + if event.Data != nil { + data.Title = event.Data.Title() } -} -func (m *StoreManager) removeFromIndex(event *Event) { - if m.index == nil { - return - } - _ = m.index.Delete(event.Id) + m.index.Add(event.Id, data) } func GetSearchResult(fields map[string]string, _ []string) (search.ResultItem, error) { @@ -62,6 +54,9 @@ func GetSearchResult(fields map[string]string, _ []string) (search.ResultItem, e } func getApiFromEvent(event *Event) string { + if event.Data == nil { + return "" + } f := reflect.ValueOf(event.Data).Elem().FieldByName("Api") if f.IsValid() { return f.String() diff --git a/runtime/events/index_test.go b/runtime/events/index_test.go index 96f4c0cba..6211a1d1a 100644 --- a/runtime/events/index_test.go +++ b/runtime/events/index_test.go @@ -1,13 +1,17 @@ package events_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/static" "mokapi/runtime" "mokapi/runtime/events" "mokapi/runtime/events/eventstest" "mokapi/runtime/search" + "mokapi/safe" "testing" + "time" + + "github.com/stretchr/testify/require" ) func TestIndex_Http(t *testing.T) { @@ -28,8 +32,12 @@ func TestIndex_Http(t *testing.T) { }, trait) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, "Event", r.Results[0].Type) @@ -52,8 +60,12 @@ func TestIndex_Http(t *testing.T) { }, trait) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "type:event", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "type:event", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, "Event", r.Results[0].Type) @@ -75,7 +87,6 @@ func TestIndex_Http(t *testing.T) { Api: "My API", }, trait) require.NoError(t, err) - _, err = app.Search(search.Request{QueryText: "type:", Limit: 10}) require.Error(t, err) }, @@ -98,8 +109,15 @@ func TestIndex_Http(t *testing.T) { }, trait) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "type:event", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "type:event", Limit: 10}) + require.NoError(t, err) + if len(r.Results) == 0 { + return false + } + return r.Results[0].Title == "bar" + }) require.Len(t, r.Results, 1) require.Equal(t, "Event", r.Results[0].Type) @@ -121,11 +139,31 @@ func TestIndex_Http(t *testing.T) { &static.Config{ Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }, }, }) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } } + +func waitSearchIndex(t *testing.T, check func() bool) { + deadline := time.Now().Add(2 * time.Second) + + for { + if check() { + break + } + if time.Now().After(deadline) { + t.Fatal("wait search index reached deadline") + } + time.Sleep(20 * time.Millisecond) + } +} diff --git a/runtime/index.go b/runtime/index.go index 3d0de0d68..290934629 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -1,6 +1,16 @@ package runtime import ( + "context" + "mokapi/config/static" + "mokapi/runtime/events" + "mokapi/runtime/search" + "mokapi/safe" + "os" + "path/filepath" + "regexp" + "slices" + "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" "github.com/blevesearch/bleve/v2/analysis/char/asciifolding" @@ -15,11 +25,6 @@ import ( "github.com/blevesearch/bleve/v2/search/query" index "github.com/blevesearch/bleve_index_api" log "github.com/sirupsen/logrus" - "mokapi/config/static" - "mokapi/runtime/events" - "mokapi/runtime/search" - "regexp" - "slices" "strings" ) @@ -27,9 +32,25 @@ import ( var fieldsNotIncludedInAll = []string{"api"} var SupportedFacets = []string{"type"} -func newIndex(cfg *static.Config) bleve.Index { - if !cfg.Api.Search.Enabled { - return nil +type SearchIndex struct { + cfg static.Search + idx bleve.Index + ready chan struct{} + queue chan func() +} + +func newSearchIndex(cfg static.Search) *SearchIndex { + s := &SearchIndex{cfg: cfg} + if cfg.Enabled { + s.ready = make(chan struct{}) + s.queue = make(chan func(), 1000) + } + return s +} + +func (s *SearchIndex) start(pool *safe.Pool) { + if !s.cfg.Enabled { + return } // 💡 Disable indexing for "_title" @@ -74,25 +95,89 @@ func newIndex(cfg *static.Config) bleve.Index { panic(err) } - idx, err := bleve.NewMemOnly(mapping) + if !s.cfg.InMemory { + indexPath := getSearchIndexPath(s.cfg) + _ = os.RemoveAll(indexPath) + s.idx, err = bleve.New(indexPath, mapping) + } else { + s.idx, err = bleve.NewMemOnly(mapping) + } + if err != nil { - log.Error(err) + log.Errorf("disabling search due to error: %s", err) + s.cfg.Enabled = false + close(s.ready) + close(s.queue) + return + } + +initialization: + for { + select { + case op := <-s.queue: + op() + default: + close(s.ready) + break initialization + } } - return idx + pool.Go(func(ctx context.Context) { + for { + select { + case op, ok := <-s.queue: + if !ok { + return + } + op() + case <-ctx.Done(): + close(s.queue) + + indexPath := getSearchIndexPath(s.cfg) + if indexPath != "" { + _ = os.RemoveAll(indexPath) + } + + return + } + } + }) } -func add(index bleve.Index, id string, data any) { - err := index.Index(id, data) +func (s *SearchIndex) Add(id string, data any) { + if !s.cfg.Enabled { + return + } + s.queue <- func() { + s.add(id, data) + } +} + +func (s *SearchIndex) add(id string, data any) { + if s.idx == nil { + return + } + err := s.idx.Index(id, data) if err != nil { log.Errorf("add '%s' to search index failed: %v", id, err) } } -func (a *App) Search(r search.Request) (search.Result, error) { +func (s *SearchIndex) Delete(id string) { + if !s.cfg.Enabled { + return + } + s.queue <- func() { + _ = s.idx.Delete(id) + } +} + +func (s *SearchIndex) Search(r search.Request) (search.Result, error) { result := search.Result{} - if a.index == nil { + <-s.ready + + if s.idx == nil || !s.cfg.Enabled { return result, &search.ErrNotEnabled{} } @@ -127,7 +212,7 @@ func (a *App) Search(r search.Request) (search.Result, error) { sr.SortBy([]string{"-_score", "_id"}) sr.Highlight = bleve.NewHighlightWithStyle(html.Name) - searchResult, err := a.index.Search(sr) + searchResult, err := s.idx.Search(sr) if err != nil { return result, err } @@ -136,10 +221,13 @@ func (a *App) Search(r search.Request) (search.Result, error) { for _, hit := range searchResult.Hits { item := search.ResultItem{} - doc, err := a.index.Document(hit.ID) + doc, err := s.idx.Document(hit.ID) if err != nil { return result, err } + if doc == nil { + continue + } fields := getSearchFields(doc) discriminators := strings.Split(fields["discriminator"], "_") @@ -182,7 +270,7 @@ func (a *App) Search(r search.Request) (search.Result, error) { sr = bleve.NewSearchRequest(q) sr.Size = 0 sr.AddFacet("type", bleve.NewFacetRequest("type", 6)) - searchResult, err = a.index.Search(sr) + searchResult, err = s.idx.Search(sr) if err != nil { return result, err } @@ -261,3 +349,15 @@ func getTypeFacet(term *bleveSearch.TermFacet) search.FacetValue { } return facet } + +func getSearchIndexPath(cfg static.Search) string { + if cfg.InMemory { + return "" + } + + indexPath := cfg.IndexPath + if indexPath == "" { + indexPath = os.TempDir() + } + return filepath.Join(cfg.IndexPath, "mokapi-bleve-index") +} diff --git a/runtime/index_test.go b/runtime/index_test.go index 01477a925..dd918b623 100644 --- a/runtime/index_test.go +++ b/runtime/index_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/static" "mokapi/providers/openapi/openapitest" @@ -9,8 +9,11 @@ import ( "mokapi/runtime/events" "mokapi/runtime/events/eventstest" "mokapi/runtime/search" + "mokapi/safe" "mokapi/try" "testing" + + "github.com/stretchr/testify/require" ) func TestIndex(t *testing.T) { @@ -18,6 +21,10 @@ func TestIndex(t *testing.T) { cfg.Api.Search.Enabled = true app := runtime.New(cfg) + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + trait := events.NewTraits().WithNamespace("test") app.Events.SetStore(10, trait) err := app.Events.Push(&eventstest.Event{ diff --git a/runtime/runtime.go b/runtime/runtime.go index 791745899..70741a62a 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -1,12 +1,12 @@ package runtime import ( - "github.com/blevesearch/bleve/v2" - log "github.com/sirupsen/logrus" "mokapi/config/dynamic" "mokapi/config/static" "mokapi/runtime/events" "mokapi/runtime/monitor" + "mokapi/runtime/search" + "mokapi/safe" "mokapi/version" "sync" ) @@ -23,9 +23,9 @@ type App struct { Monitor *monitor.Monitor Events *events.StoreManager - m sync.Mutex - cfg *static.Config - index bleve.Index + m sync.Mutex + cfg *static.Config + searchIndex *SearchIndex Configs map[string]*dynamic.Config } @@ -33,7 +33,7 @@ type App struct { func New(cfg *static.Config) *App { m := monitor.New() - index := newIndex(cfg) + index := newSearchIndex(cfg.Api.Search) em := events.NewStoreManager(index) em.SetStore(int(cfg.Event.Store["default"].Size), events.NewTraits().WithNamespace("http")) @@ -43,23 +43,27 @@ func New(cfg *static.Config) *App { em.SetStore(int(cfg.Event.Store["default"].Size), events.NewTraits().WithNamespace("job")) app := &App{ - Version: version.BuildVersion, - BuildTime: version.BuildTime, - Monitor: m, - Events: em, - Configs: map[string]*dynamic.Config{}, - http: NewHttpStore(cfg, index, em), - Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em}, - Mqtt: &MqttStore{monitor: m, cfg: cfg, sm: em}, - Ldap: &LdapStore{cfg: cfg, events: em, index: index}, - Mail: &MailStore{cfg: cfg, sm: em, index: index}, - cfg: cfg, - index: index, + Version: version.BuildVersion, + BuildTime: version.BuildTime, + Monitor: m, + Events: em, + Configs: map[string]*dynamic.Config{}, + http: NewHttpStore(cfg, index, em), + Kafka: &KafkaStore{monitor: m, cfg: cfg, index: index, events: em}, + Mqtt: &MqttStore{monitor: m, cfg: cfg, sm: em}, + Ldap: &LdapStore{cfg: cfg, events: em, index: index}, + Mail: &MailStore{cfg: cfg, sm: em, index: index}, + cfg: cfg, + searchIndex: index, } return app } +func (a *App) Start(p *safe.Pool) { + go a.searchIndex.start(p) +} + func (a *App) UpdateConfig(e dynamic.ConfigEvent) { a.m.Lock() defer a.m.Unlock() @@ -74,10 +78,8 @@ func (a *App) UpdateConfig(e dynamic.ConfigEvent) { } if a.cfg.Api.Search.Enabled { - removeConfigFromIndex(a.index, e.Config) - if err := addConfigToIndex(a.index, e.Config); err != nil { - log.Errorf("add '%s' to search index failed", e.Config.Info.Path()) - } + a.removeConfigFromIndex(e.Config) + a.addConfigToIndex(e.Config) } } @@ -113,3 +115,7 @@ func (a *App) RemoveHttp(c *dynamic.Config) { func (a *App) ListHttp() []*HttpInfo { return a.http.List() } + +func (a *App) Search(r search.Request) (search.Result, error) { + return a.searchIndex.Search(r) +} diff --git a/runtime/runtime_http.go b/runtime/runtime_http.go index ea0b643be..bd55b3c3b 100644 --- a/runtime/runtime_http.go +++ b/runtime/runtime_http.go @@ -7,12 +7,12 @@ import ( "mokapi/providers/openapi" "mokapi/runtime/events" "mokapi/runtime/monitor" + "mokapi/runtime/search" "net/http" "path/filepath" "sort" "sync" - "github.com/blevesearch/bleve/v2" log "github.com/sirupsen/logrus" ) @@ -20,7 +20,7 @@ type HttpStore struct { infos map[string]*HttpInfo cfg *static.Config m sync.RWMutex - index bleve.Index + index search.Index events *events.StoreManager } @@ -35,7 +35,7 @@ type httpHandler struct { next openapi.Handler } -func NewHttpStore(cfg *static.Config, index bleve.Index, em *events.StoreManager) *HttpStore { +func NewHttpStore(cfg *static.Config, index search.Index, em *events.StoreManager) *HttpStore { s := &HttpStore{ cfg: cfg, index: index, diff --git a/runtime/runtime_http_search.go b/runtime/runtime_http_search.go index f369ed666..e6a551da1 100644 --- a/runtime/runtime_http_search.go +++ b/runtime/runtime_http_search.go @@ -73,7 +73,7 @@ func (s *HttpStore) addToIndex(cfg *openapi.Config) { Servers: cfg.Servers, } - add(s.index, fmt.Sprintf("http_%s", cfg.Info.Name), c) + s.index.Add(fmt.Sprintf("http_%s", cfg.Info.Name), c) for path, p := range cfg.Paths { if p.Value == nil { @@ -104,7 +104,7 @@ func (s *HttpStore) addToIndex(cfg *openapi.Config) { }) } - add(s.index, fmt.Sprintf("http_%s_%s", cfg.Info.Name, path), pathData) + s.index.Add(fmt.Sprintf("http_%s_%s", cfg.Info.Name, path), pathData) for method, op := range p.Value.Operations() { id := fmt.Sprintf("http_%s_%s_%s", cfg.Info.Name, path, method) @@ -150,7 +150,7 @@ func (s *HttpStore) addToIndex(cfg *openapi.Config) { } } - add(s.index, id, opData) + s.index.Add(id, opData) } } } @@ -194,12 +194,12 @@ func getHttpSearchResult(fields map[string]string, discriminator []string) (sear } func (s *HttpStore) removeFromIndex(cfg *openapi.Config) { - _ = s.index.Delete(fmt.Sprintf("http_%s", cfg.Info.Name)) + s.index.Delete(fmt.Sprintf("http_%s", cfg.Info.Name)) for path, p := range cfg.Paths { - _ = s.index.Delete(fmt.Sprintf("http_%s_%s", cfg.Info.Name, path)) + s.index.Delete(fmt.Sprintf("http_%s_%s", cfg.Info.Name, path)) for method := range p.Value.Operations() { - _ = s.index.Delete(fmt.Sprintf("http_%s_%s_%s", cfg.Info.Name, path, method)) + s.index.Delete(fmt.Sprintf("http_%s_%s_%s", cfg.Info.Name, path, method)) } } } diff --git a/runtime/runtime_http_search_test.go b/runtime/runtime_http_search_test.go index 9fc3af715..74494a172 100644 --- a/runtime/runtime_http_search_test.go +++ b/runtime/runtime_http_search_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" @@ -9,7 +9,11 @@ import ( "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "testing" + "time" + + "github.com/stretchr/testify/require" ) func TestIndex_Http(t *testing.T) { @@ -30,8 +34,14 @@ func TestIndex_Http(t *testing.T) { test: func(t *testing.T, app *runtime.App) { cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "", "")) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -55,8 +65,11 @@ func TestIndex_Http(t *testing.T) { require.NoError(t, err) require.Len(t, r.Results, 1) app.RemoveHttp(toConfig(cfg)) - r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.Len(t, r.Results, 0) + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 0 + }) }, }, { @@ -64,8 +77,14 @@ func TestIndex_Http(t *testing.T) { test: func(t *testing.T, app *runtime.App) { cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("My petstore API", "", "")) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "pet*", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "pet*", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -85,8 +104,14 @@ func TestIndex_Http(t *testing.T) { test: func(t *testing.T, app *runtime.App) { cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("mailbox", "", "")) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "mailpiece", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "mailpiece", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 0 + }) require.Len(t, r.Results, 0) }, }, @@ -95,8 +120,14 @@ func TestIndex_Http(t *testing.T) { test: func(t *testing.T, app *runtime.App) { cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo", "1.0", "")) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "1.0", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "1.0", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -119,8 +150,14 @@ func TestIndex_Http(t *testing.T) { openapitest.WithPath("/pets", openapitest.NewPath()), ) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "pets", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "pets", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -145,8 +182,14 @@ func TestIndex_Http(t *testing.T) { openapitest.WithPath("/pets", openapitest.NewPath(openapitest.WithPathInfo("", "a description"))), ) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "description", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "description", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 2 + }) require.Len(t, r.Results, 2) require.Equal(t, search.ResultItem{ @@ -187,8 +230,14 @@ func TestIndex_Http(t *testing.T) { )), ) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "\"parameter description\"", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "\"parameter description\"", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -219,8 +268,13 @@ func TestIndex_Http(t *testing.T) { ) app.AddHttp(toConfig(cfg)) // search response should only have one the root OpenAPI object - r, err := app.Search(search.Request{QueryText: "Petstore", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Petstore", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) // search by api should return all items in the OpenAPI @@ -245,8 +299,14 @@ func TestIndex_Http(t *testing.T) { test: func(t *testing.T, app *runtime.App) { cfg := openapitest.NewConfig("3.0", openapitest.WithInfo("foo bar", "", "")) app.AddHttp(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "api:\"foo bar\"", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "api:\"foo bar\"", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -274,8 +334,13 @@ func TestIndex_Http(t *testing.T) { ) app.AddHttp(toConfig(cfg)) // search response should only have one the root OpenAPI object - r, err := app.Search(search.Request{QueryText: "Petstore", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Petstore", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) // search by api should return all items in the OpenAPI @@ -308,8 +373,13 @@ func TestIndex_Http(t *testing.T) { ) app.AddHttp(toConfig(cfg)) // search response should only have one the root OpenAPI object - r, err := app.Search(search.Request{QueryText: "pest~", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "pest~", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 2 + }) require.Len(t, r.Results, 2) }, }, @@ -324,11 +394,31 @@ func TestIndex_Http(t *testing.T) { &static.Config{ Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }, }, }) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } } + +func waitSearchIndex(t *testing.T, check func() bool) { + deadline := time.Now().Add(2 * time.Second) + + for { + if check() { + break + } + if time.Now().After(deadline) { + t.Fatal("wait search index reached deadline") + } + time.Sleep(20 * time.Millisecond) + } +} diff --git a/runtime/runtime_kafka.go b/runtime/runtime_kafka.go index 4209709c8..821979851 100644 --- a/runtime/runtime_kafka.go +++ b/runtime/runtime_kafka.go @@ -10,12 +10,12 @@ import ( "mokapi/providers/asyncapi3/kafka/store" "mokapi/runtime/events" "mokapi/runtime/monitor" + "mokapi/runtime/search" "mokapi/sortedmap" "path/filepath" "sort" "sync" - "github.com/blevesearch/bleve/v2" log "github.com/sirupsen/logrus" ) @@ -24,7 +24,7 @@ type KafkaStore struct { monitor *monitor.Monitor cfg *static.Config events *events.StoreManager - index bleve.Index + index search.Index m sync.RWMutex } diff --git a/runtime/runtime_kafka_search.go b/runtime/runtime_kafka_search.go index e7a4b595f..e787c9c0c 100644 --- a/runtime/runtime_kafka_search.go +++ b/runtime/runtime_kafka_search.go @@ -81,7 +81,7 @@ func (s *KafkaStore) addToIndex(cfg *asyncapi3.Config) { }) } - add(s.index, fmt.Sprintf("kafka_%s", cfg.Info.Name), c) + s.index.Add(fmt.Sprintf("kafka_%s", cfg.Info.Name), c) for name, topic := range cfg.Channels { if topic == nil || topic.Value == nil { @@ -124,7 +124,7 @@ func (s *KafkaStore) addToIndex(cfg *asyncapi3.Config) { }) } id := fmt.Sprintf("kafka_%s_%s", cfg.Info.Name, name) - add(s.index, id, t) + s.index.Add(id, t) } } @@ -176,9 +176,9 @@ func getSchema(s *asyncapi3.SchemaRef) (*schema.IndexData, error) { } func (s *KafkaStore) removeFromIndex(cfg *asyncapi3.Config) { - _ = s.index.Delete(fmt.Sprintf("kafka_%s", cfg.Info.Name)) + s.index.Delete(fmt.Sprintf("kafka_%s", cfg.Info.Name)) for name := range cfg.Channels { - _ = s.index.Delete(fmt.Sprintf("kafka_%s_%s", cfg.Info.Name, name)) + s.index.Delete(fmt.Sprintf("kafka_%s_%s", cfg.Info.Name, name)) } } diff --git a/runtime/runtime_kafka_search_test.go b/runtime/runtime_kafka_search_test.go index f112b9588..a647ee840 100644 --- a/runtime/runtime_kafka_search_test.go +++ b/runtime/runtime_kafka_search_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" @@ -10,7 +10,10 @@ import ( "mokapi/providers/asyncapi3/asyncapi3test" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "testing" + + "github.com/stretchr/testify/require" ) func TestIndex_Kafka(t *testing.T) { @@ -33,8 +36,12 @@ func TestIndex_Kafka(t *testing.T) { _, err := app.Kafka.Add(toConfig(cfg), enginetest.NewEngine()) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "Test", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -56,13 +63,20 @@ func TestIndex_Kafka(t *testing.T) { _, err := app.Kafka.Add(toConfig(cfg), enginetest.NewEngine()) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "Test", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) app.Kafka.Remove(toConfig(cfg)) - r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) - require.NoError(t, err) + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 0 + }) require.Len(t, r.Results, 0) }, }, @@ -78,8 +92,12 @@ func TestIndex_Kafka(t *testing.T) { _, err := app.Kafka.Add(toConfig(cfg), enginetest.NewEngine()) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "description", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "description", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -106,10 +124,16 @@ func TestIndex_Kafka(t *testing.T) { &static.Config{ Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }, }, }) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } diff --git a/runtime/runtime_ldap.go b/runtime/runtime_ldap.go index 0f0096cdf..40daceb0c 100644 --- a/runtime/runtime_ldap.go +++ b/runtime/runtime_ldap.go @@ -9,12 +9,12 @@ import ( "mokapi/providers/directory" "mokapi/runtime/events" "mokapi/runtime/monitor" + "mokapi/runtime/search" "path/filepath" "sort" "sync" "time" - "github.com/blevesearch/bleve/v2" log "github.com/sirupsen/logrus" ) @@ -23,7 +23,7 @@ type LdapStore struct { cfg *static.Config events *events.StoreManager m sync.RWMutex - index bleve.Index + index search.Index } type LdapInfo struct { diff --git a/runtime/runtime_ldap_search.go b/runtime/runtime_ldap_search.go index 8090df84a..af43a7496 100644 --- a/runtime/runtime_ldap_search.go +++ b/runtime/runtime_ldap_search.go @@ -46,7 +46,7 @@ func (s *LdapStore) addToIndex(cfg *directory.Config) { Server: cfg.Address, } - add(s.index, fmt.Sprintf("ldap_%s", cfg.Info.Name), c) + s.index.Add(fmt.Sprintf("ldap_%s", cfg.Info.Name), c) if cfg.Entries != nil { for it := cfg.Entries.Iter(); it.Next(); { @@ -63,7 +63,7 @@ func (s *LdapStore) addToIndex(cfg *directory.Config) { Values: values, }) } - add(s.index, fmt.Sprintf("mail_%s_%s", cfg.Info.Name, e.Dn), se) + s.index.Add(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, e.Dn), se) } } } @@ -97,12 +97,12 @@ func getLdapSearchResult(fields map[string]string, discriminator []string) (sear } func (s *LdapStore) removeFromIndex(cfg *directory.Config) { - _ = s.index.Delete(fmt.Sprintf("ldap_%s", cfg.Info.Name)) + s.index.Delete(fmt.Sprintf("ldap_%s", cfg.Info.Name)) if cfg.Entries != nil { for it := cfg.Entries.Iter(); it.Next(); { e := it.Value() - _ = s.index.Delete(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, e.Dn)) + s.index.Delete(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, e.Dn)) } } } diff --git a/runtime/runtime_ldap_search_test.go b/runtime/runtime_ldap_search_test.go index bac084bcf..4a488d842 100644 --- a/runtime/runtime_ldap_search_test.go +++ b/runtime/runtime_ldap_search_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" @@ -9,8 +9,11 @@ import ( "mokapi/providers/directory" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "mokapi/sortedmap" "testing" + + "github.com/stretchr/testify/require" ) func TestIndex_Ldap(t *testing.T) { @@ -33,8 +36,13 @@ func TestIndex_Ldap(t *testing.T) { Info: directory.Info{Name: "foo"}, } app.Ldap.Add(toConfig(cfg), enginetest.NewEngine()) - r, err := app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -56,13 +64,21 @@ func TestIndex_Ldap(t *testing.T) { Info: directory.Info{Name: "foo"}, } app.Ldap.Add(toConfig(cfg), enginetest.NewEngine()) - r, err := app.Search(search.Request{Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) app.Ldap.Remove(toConfig(cfg)) - r, err = app.Search(search.Request{Limit: 10}) - require.NoError(t, err) + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 0 + }) require.Len(t, r.Results, 0) }, }, @@ -80,8 +96,13 @@ func TestIndex_Ldap(t *testing.T) { Entries: entries, } app.Ldap.Add(toConfig(cfg), enginetest.NewEngine()) - r, err := app.Search(search.Request{QueryText: "alice", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "alice", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -116,10 +137,16 @@ func TestIndex_Ldap(t *testing.T) { &static.Config{ Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }, }, }) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } diff --git a/runtime/runtime_mail.go b/runtime/runtime_mail.go index 5015aebd9..df9da9ba5 100644 --- a/runtime/runtime_mail.go +++ b/runtime/runtime_mail.go @@ -2,8 +2,6 @@ package runtime import ( "context" - "github.com/blevesearch/bleve/v2" - log "github.com/sirupsen/logrus" "mokapi/config/dynamic" "mokapi/config/static" engine "mokapi/engine/common" @@ -11,11 +9,14 @@ import ( "mokapi/providers/mail" "mokapi/runtime/events" "mokapi/runtime/monitor" + "mokapi/runtime/search" "mokapi/smtp" "path/filepath" "sort" "sync" "time" + + log "github.com/sirupsen/logrus" ) type MailHandler interface { @@ -28,7 +29,7 @@ type MailStore struct { m sync.RWMutex cfg *static.Config sm *events.StoreManager - index bleve.Index + index search.Index } type MailInfo struct { diff --git a/runtime/runtime_mail_search.go b/runtime/runtime_mail_search.go index 18c82f316..77a9a6ecf 100644 --- a/runtime/runtime_mail_search.go +++ b/runtime/runtime_mail_search.go @@ -64,7 +64,7 @@ func (s *MailStore) addToIndex(cfg *mail.Config) { }) } - add(s.index, fmt.Sprintf("mail_%s", cfg.Info.Name), c) + s.index.Add(fmt.Sprintf("mail_%s", cfg.Info.Name), c) for name, mb := range cfg.Mailboxes { mbi := mailSearchIndexMailbox{ @@ -79,7 +79,7 @@ func (s *MailStore) addToIndex(cfg *mail.Config) { for n, f := range mb.Folders { mbi.Folders = append(mbi.Folders, getMailboxFolders(f, n)...) } - add(s.index, fmt.Sprintf("mail_%s_%s", cfg.Info.Name, name), mbi) + s.index.Add(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, name), mbi) } } @@ -113,10 +113,10 @@ func getMailSearchResult(fields map[string]string, discriminator []string) (sear } func (s *MailStore) removeFromIndex(cfg *mail.Config) { - _ = s.index.Delete(fmt.Sprintf("mail_%s", cfg.Info.Name)) + s.index.Delete(fmt.Sprintf("mail_%s", cfg.Info.Name)) for name := range cfg.Mailboxes { - _ = s.index.Delete(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, name)) + s.index.Delete(fmt.Sprintf("mail_%s_%s", cfg.Info.Name, name)) } } diff --git a/runtime/runtime_mail_search_test.go b/runtime/runtime_mail_search_test.go index 330b02662..dc08e0035 100644 --- a/runtime/runtime_mail_search_test.go +++ b/runtime/runtime_mail_search_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" @@ -9,7 +9,10 @@ import ( "mokapi/providers/mail" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "testing" + + "github.com/stretchr/testify/require" ) func TestIndex_Mail(t *testing.T) { @@ -32,8 +35,13 @@ func TestIndex_Mail(t *testing.T) { Info: mail.Info{Name: "foo"}, } app.Mail.Add(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -65,13 +73,22 @@ func TestIndex_Mail(t *testing.T) { }, } app.Mail.Add(toConfig(cfg)) - r, err := app.Search(search.Request{Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 2 + }) require.Len(t, r.Results, 2) app.Mail.Remove(toConfig(cfg)) - r, err = app.Search(search.Request{Limit: 10}) - require.NoError(t, err) + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "Test", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 0 + }) require.Len(t, r.Results, 0) }, }, @@ -92,8 +109,14 @@ func TestIndex_Mail(t *testing.T) { }, } app.Mail.Add(toConfig(cfg)) - r, err := app.Search(search.Request{QueryText: "alice", Limit: 10}) - require.NoError(t, err) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "alice", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -134,10 +157,16 @@ func TestIndex_Mail(t *testing.T) { &static.Config{ Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }, }, }) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } diff --git a/runtime/runtime_search.go b/runtime/runtime_search.go index 1f11f561a..b2947ea0b 100644 --- a/runtime/runtime_search.go +++ b/runtime/runtime_search.go @@ -1,7 +1,6 @@ package runtime import ( - "github.com/blevesearch/bleve/v2" "mokapi/config/dynamic" "mokapi/runtime/search" "strings" @@ -15,8 +14,8 @@ type config struct { Data string `json:"data"` } -func addConfigToIndex(index bleve.Index, cfg *dynamic.Config) error { - return index.Index(cfg.Info.Key(), config{ +func (a *App) addConfigToIndex(cfg *dynamic.Config) { + a.searchIndex.Add(cfg.Info.Key(), config{ Discriminator: "config", Provider: cfg.Info.Provider, Name: cfg.Info.Path(), @@ -25,8 +24,8 @@ func addConfigToIndex(index bleve.Index, cfg *dynamic.Config) error { }) } -func removeConfigFromIndex(index bleve.Index, cfg *dynamic.Config) { - _ = index.Delete(cfg.Info.Key()) +func (a *App) removeConfigFromIndex(cfg *dynamic.Config) { + a.searchIndex.Delete(cfg.Info.Key()) } func getConfigSearchResult(fields map[string]string, _ []string) (search.ResultItem, error) { diff --git a/runtime/runtime_search_test.go b/runtime/runtime_search_test.go index 80bc4be0b..bf1d0670c 100644 --- a/runtime/runtime_search_test.go +++ b/runtime/runtime_search_test.go @@ -1,7 +1,7 @@ package runtime_test import ( - "github.com/stretchr/testify/require" + "context" "mokapi/config/dynamic" "mokapi/config/dynamic/asyncApi/asyncapitest" "mokapi/config/dynamic/dynamictest" @@ -10,7 +10,10 @@ import ( "mokapi/providers/openapi/openapitest" "mokapi/runtime" "mokapi/runtime/search" + "mokapi/safe" "testing" + + "github.com/stretchr/testify/require" ) func TestIndex_Config(t *testing.T) { @@ -42,8 +45,13 @@ func TestIndex_Config(t *testing.T) { Config: cfg, Event: dynamic.Create, }) - r, err := app.Search(search.Request{QueryText: "name", Limit: 10}) - require.NoError(t, err) + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "name", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 1 + }) require.Len(t, r.Results, 1) require.Equal(t, search.ResultItem{ @@ -68,8 +76,12 @@ func TestIndex_Config(t *testing.T) { _, err := app.Kafka.Add(toConfig(k), enginetest.NewEngine()) require.NoError(t, err) - r, err := app.Search(search.Request{QueryText: "foo", Limit: 10}) - require.NoError(t, err) + var r search.Result + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "foo", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 2 + }) require.Len(t, r.Results, 2) }, }, @@ -82,8 +94,14 @@ func TestIndex_Config(t *testing.T) { t.Parallel() app := runtime.New(&static.Config{Api: static.Api{ Search: static.Search{ - Enabled: true, + Enabled: true, + InMemory: true, }}}) + + pool := safe.NewPool(context.Background()) + app.Start(pool) + defer pool.Stop() + tc.test(t, app) }) } diff --git a/runtime/search/index.go b/runtime/search/index.go index 90a77e787..38ac1ce2d 100644 --- a/runtime/search/index.go +++ b/runtime/search/index.go @@ -1,5 +1,10 @@ package search +type Index interface { + Add(id string, data any) + Delete(id string) +} + type Result struct { Results []ResultItem `json:"results"` Facets map[string][]FacetValue `json:"facets,omitempty"` diff --git a/server/server.go b/server/server.go index f28bf79f0..9697099d2 100644 --- a/server/server.go +++ b/server/server.go @@ -1,10 +1,11 @@ package server import ( - log "github.com/sirupsen/logrus" "mokapi/engine" "mokapi/runtime" "mokapi/safe" + + log "github.com/sirupsen/logrus" ) type Server struct { @@ -36,6 +37,7 @@ func NewServer(pool *safe.Pool, app *runtime.App, watcher *ConfigWatcher, } func (s *Server) Start() error { + s.app.Start(s.pool) s.engine.Start() if err := s.watcher.Start(s.pool); err != nil { return err diff --git a/webui/package-lock.json b/webui/package-lock.json index aa7fdc2e8..f78ffd67d 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -11,8 +11,8 @@ "@popperjs/core": "^2.11.6", "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", - "@types/mokapi": "^0.29.1", - "@types/nodemailer": "^7.0.9", + "@types/mokapi": "^0.34.0", + "@types/nodemailer": "^7.0.10", "@types/whatwg-mimetype": "^3.0.2", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -40,13 +40,13 @@ }, "devDependencies": { "@playwright/test": "^1.58.2", - "@rushstack/eslint-patch": "^1.15.0", + "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", "@types/node": "^25.2.3", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", - "@vue/eslint-config-typescript": "^14.6.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", "eslint-plugin-vue": "^10.6.2", @@ -1270,9 +1270,9 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true, "license": "MIT" }, @@ -1368,9 +1368,9 @@ "license": "MIT" }, "node_modules/@types/mokapi": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@types/mokapi/-/mokapi-0.29.1.tgz", - "integrity": "sha512-Ozcf/eO6QDc3VnEKEDRa2S9POT3eis97kYZUqKSsnVN9IYyb/j3jbmDbWsjzeU/pYegwcNfSqGwFmUc+/1n0qg==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@types/mokapi/-/mokapi-0.34.0.tgz", + "integrity": "sha512-AtSfdd/aZDhIaKYdtyq5v05JuJijQhMVVu1XuIosnkCtkRerK1JIm/k6ibnJy6Taxez+ztSLgYdEd1ij05YORQ==", "license": "MIT" }, "node_modules/@types/node": { @@ -1383,9 +1383,9 @@ } }, "node_modules/@types/nodemailer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.10.tgz", + "integrity": "sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1398,17 +1398,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", - "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/type-utils": "8.53.1", - "@typescript-eslint/utils": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1421,8 +1421,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.1", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -1437,16 +1437,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", - "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "engines": { @@ -1457,19 +1457,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", - "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.1", - "@typescript-eslint/types": "^8.53.1", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -1484,14 +1484,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", - "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1502,9 +1502,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", - "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -1519,15 +1519,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", - "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1539,14 +1539,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", - "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -1558,16 +1558,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", - "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.53.1", - "@typescript-eslint/tsconfig-utils": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -1586,16 +1586,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", - "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1605,19 +1605,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", - "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1628,13 +1628,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1812,22 +1812,22 @@ } }, "node_modules/@vue/eslint-config-typescript": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.6.0.tgz", - "integrity": "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", + "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.35.1", + "@typescript-eslint/utils": "^8.56.0", "fast-glob": "^3.3.3", - "typescript-eslint": "^8.35.1", - "vue-eslint-parser": "^10.2.0" + "typescript-eslint": "^8.56.0", + "vue-eslint-parser": "^10.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^9.10.0", + "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, @@ -7068,16 +7068,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", - "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1" + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7087,7 +7087,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -7390,16 +7390,16 @@ } }, "node_modules/vue-eslint-parser": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", - "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, @@ -7410,7 +7410,7 @@ "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { diff --git a/webui/package.json b/webui/package.json index bcc620d7c..661e7e17a 100644 --- a/webui/package.json +++ b/webui/package.json @@ -20,8 +20,8 @@ "@popperjs/core": "^2.11.6", "@ssthouse/vue3-tree-chart": "^0.3.0", "@types/bootstrap": "^5.2.10", - "@types/mokapi": "^0.29.1", - "@types/nodemailer": "^7.0.9", + "@types/mokapi": "^0.34.0", + "@types/nodemailer": "^7.0.10", "@types/whatwg-mimetype": "^3.0.2", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", @@ -49,13 +49,13 @@ }, "devDependencies": { "@playwright/test": "^1.58.2", - "@rushstack/eslint-patch": "^1.15.0", + "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", "@types/node": "^25.2.3", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", - "@vue/eslint-config-typescript": "^14.6.0", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", "eslint-plugin-vue": "^10.6.2", diff --git a/webui/public/.htaccess b/webui/public/.htaccess index 06b37a4c7..b17ce6370 100644 --- a/webui/public/.htaccess +++ b/webui/public/.htaccess @@ -29,6 +29,12 @@ # Redirect anything under /docs/resources/ to /resources/ RewriteRule ^docs/resources/(.*)$ /resources/$1 [R=301,L] + # Redirect old guides like /docs/guides/http to /docs/http/overview + RewriteRule ^docs/guides/http/?$ /docs/http/overview [R=301,L] + RewriteRule ^docs/guides/kafka/?$ /docs/kafka/overview [R=301,L] + RewriteRule ^docs/guides/ldap/?$ /docs/ldap/overview [R=301,L] + RewriteRule ^docs/guides/mail/?$ /docs/mail/overview [R=301,L] + # Redirect anything under /docs/guides/ to /docs/ RewriteRule ^docs/guides/(.*)$ /docs/$1 [R=301,L] @@ -36,7 +42,11 @@ RewriteRule ^smtp/?$ /mail [R=301,L] # Redirect anything under /docs/guides/smtp to /docs/mail/ - RewriteRule ^docs/guides/smtp/?(.*)$ /docs/mail/$1 [R=301,L] + RewriteRule ^docs/smtp/?(.*)$ /docs/mail/$1 [R=301,L] + + RewriteRule ^docs/http/https-&-tls/?$ /docs/http/tls [R=301,L] + RewriteRule ^resources/blogs/ensuring-api-contract-compliance-with-mokapi$ /resources/blogs/guard-your-api-contracts [R=301,L] + RewriteRule ^resources/blogs/acceptance-testing-with-mokapi:-focus-on-what-matters$ /resources/blogs/acceptance-testing [R=301,L] # rewrite for bots RewriteCond %{HTTP_USER_AGENT} googlebot|bingbot|Seobility|yandex|baiduspider|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora\ link\ preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp|redditbot|applebot|flipboard|tumblr|bitlybot|skypeuripreview|nuzzel|discordbot|google\ page\ speed|qwantify|bitrix\ link\ preview|xing-contenttabreceiver|google-inspectiontool|chrome-lighthouse|telegrambot|amazonbot [NC] diff --git a/webui/public/acceptance-testing-boundaries-mokapi.png b/webui/public/acceptance-testing-boundaries-mokapi.png index 32f364bbc..3ca589f88 100644 Binary files a/webui/public/acceptance-testing-boundaries-mokapi.png and b/webui/public/acceptance-testing-boundaries-mokapi.png differ diff --git a/webui/public/acceptance-testing-mokapi.png b/webui/public/acceptance-testing-mokapi.png index fb934dbe0..10173fdeb 100644 Binary files a/webui/public/acceptance-testing-mokapi.png and b/webui/public/acceptance-testing-mokapi.png differ diff --git a/webui/public/mokapi-using-as-proxy.png b/webui/public/mokapi-using-as-proxy.png index 31cb6a12e..1129ee42f 100644 Binary files a/webui/public/mokapi-using-as-proxy.png and b/webui/public/mokapi-using-as-proxy.png differ diff --git a/webui/src/assets/main.css b/webui/src/assets/main.css index d2f013553..b87e10bbc 100644 --- a/webui/src/assets/main.css +++ b/webui/src/assets/main.css @@ -29,6 +29,7 @@ body { #app { display: grid; grid-template-columns: auto; + grid-template-rows: 64px auto; grid-template-areas: "hd" "main" diff --git a/webui/src/assets/vars.css b/webui/src/assets/vars.css index 5f6e746e1..91cd881cb 100644 --- a/webui/src/assets/vars.css +++ b/webui/src/assets/vars.css @@ -112,6 +112,8 @@ --blockquote-background-color: var(--color-background-soft); --footer-background: #282b33; + + --color-emphasis: #f0d6cc; } [data-theme="light"] { @@ -199,4 +201,6 @@ --blockquote-background-color: #f8f9fa; --footer-background: rgb(244 244 246); + + --color-emphasis: rgb(8,109,215); } \ No newline at end of file diff --git a/webui/src/components/dashboard/http/HttpEventParameters.vue b/webui/src/components/dashboard/http/HttpEventParameters.vue index ecfd29a86..867bdaf48 100644 --- a/webui/src/components/dashboard/http/HttpEventParameters.vue +++ b/webui/src/components/dashboard/http/HttpEventParameters.vue @@ -1,10 +1,16 @@