diff --git a/.github/actions/build-release-notes/action.yml b/.github/actions/build-release-notes/action.yml
index e50f4a591..2c522f1a7 100644
--- a/.github/actions/build-release-notes/action.yml
+++ b/.github/actions/build-release-notes/action.yml
@@ -13,8 +13,3 @@ runs:
echo
echo "$BODY"
} > docs/release.md
- - name: Upload UI artifact
- uses: actions/upload-artifact@v4
- with:
- name: release-notes
- path: docs/release.md
diff --git a/api/handler_mail.go b/api/handler_mail.go
index 50e9d4136..b80fa0a46 100644
--- a/api/handler_mail.go
+++ b/api/handler_mail.go
@@ -2,7 +2,6 @@ package api
import (
"fmt"
- "mime"
"mokapi/media"
"mokapi/providers/mail"
"mokapi/runtime"
@@ -413,6 +412,11 @@ func getRejectResponse(r *mail.Rule) *rejectResponse {
}
func toMessage(m *smtp.Message) *messageData {
+ subject, err := smtp.DecodeHeaderValue(m.Subject)
+ if err != nil {
+ log.Printf("failed to decode subject '%s': %v", m.Subject, err)
+ subject = m.Subject
+ }
r := &messageData{
Server: m.Server,
From: toAddress(m.From),
@@ -423,7 +427,7 @@ func toMessage(m *smtp.Message) *messageData {
MessageId: m.MessageId,
InReplyTo: m.InReplyTo,
Date: m.Date,
- Subject: decodeSmtpValue(m.Subject),
+ Subject: subject,
ContentType: m.ContentType,
ContentTransferEncoding: m.ContentTransferEncoding,
Body: m.Body,
@@ -455,20 +459,15 @@ func toMessage(m *smtp.Message) *messageData {
func toAddress(list []smtp.Address) []address {
var r []address
for _, a := range list {
+ name, err := smtp.DecodeHeaderValue(a.Name)
+ if err != nil {
+ log.Printf("failed to decode address '%v': %v", a.Name, err)
+ name = a.Name
+ }
r = append(r, address{
- Name: decodeSmtpValue(a.Name),
+ Name: name,
Address: a.Address,
})
}
return r
}
-
-func decodeSmtpValue(s string) string {
- dec := new(mime.WordDecoder)
- r, err := dec.DecodeHeader(s)
- if err != nil {
- log.Errorf("failed to decode SMTP header: %v", err)
- return s
- }
- return r
-}
diff --git a/docs/config.json b/docs/config.json
index 92ec25bc3..a546cbf60 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -564,7 +564,7 @@
{
"label": "Mokapi behind Reverse Proxy",
"source": "resources/examples/mokapi-behind-proxy.md",
- "path": "/resources/examples/mokapi-behind-proxy",
+ "path": "/resources/examples/mokapi-behind-reverse-proxy",
"hideNavigation": true
},
{
@@ -620,7 +620,7 @@
{
"label": "Bring Your Mock APIs to Life with Mokapi and JavaScript",
"source": "resources/blogs/dynamic-mocks-with-javascript.md",
- "path": "/resources/blogs/dynamic-mocks-with-javascript",
+ "path": "/resources/blogs/bring-your-mock-apis-to-life-with-javascript",
"hideNavigation": true
},
{
diff --git a/docs/resources/blogs/dynamic-mocks-with-javascript.md b/docs/resources/blogs/dynamic-mocks-with-javascript.md
index 03ca3e387..e70458425 100644
--- a/docs/resources/blogs/dynamic-mocks-with-javascript.md
+++ b/docs/resources/blogs/dynamic-mocks-with-javascript.md
@@ -1,155 +1,130 @@
---
-title: Create Smart API Mocks with Mokapi Scripts
-description: Tired of static mocks? Learn how Mokapi Scripts let you create dynamic mock APIs using JavaScript — perfect for development, testing, and rapid prototyping.
+title: Bring Your Mock APIs to Life with JavaScript
+description: Tired of static mocks? Mokapi Scripts let you create dynamic, intelligent mock APIs that react to real request data, powered by plain JavaScript.
+subtitle: Tired of static mocks? Mokapi Scripts let you create dynamic, intelligent mock APIs that react to real request data, powered by plain JavaScript.
---
-# Bring Your Mock APIs to Life with Mokapi and JavaScript
+# Bring Your Mock APIs to Life with JavaScript
-
+Mocking APIs is essential for fast development, but static mocks can quickly become a bottleneck. What if your mock
+could think? Reacting to query parameters, headers, or body content. Simulating auth, errors, and pagination.
+Reflecting state changes across requests.
-Mocking APIs is essential for fast development — but static mocks can quickly
-become a bottleneck. Wouldn’t it be better if your mocks could think —
-reacting to queries, headers, or even generating data on the fly?
+That's exactly what Mokapi Scripts are designed for. With just a few lines of JavaScript, you can turn a flat
+JSON file into a dynamic, intelligent mock API.
-That's exactly what [Mokapi Scripts](/docs/javascript-api/overview.md) are designed for.
-
-With just a few lines of JavaScript, you can control how your mocks behave —
-making them dynamic, intelligent, and realistic.
+> No backend? No problem. With Mokapi Scripts, your mocks behave exactly the way you need them to, all in
+> familiar JavaScript or TypeScript, with no new DSL to learn.
## What Are Mokapi Scripts?
-Mokapi Scripts are lightweight JavaScript modules that give you full control
-over how your mock APIs respond. Instead of static JSON, you define behavior
-based on request data — query strings, headers, body content, and more.
-
-## Why Dynamic Mocks Matter
+Mokapi Scripts are lightweight JavaScript modules that sit alongside your OpenAPI or AsyncAPI specification.
+Instead of returning a fixed response, they let you define behavior, inspecting the incoming request and
+deciding what to send back.
-Static mock responses are fine for simple cases, but they quickly fall short when:
+They're the difference between a mock that says *"here's a user"* and one that says *"here's the right user,
+given who's asking what role they have, and what they just posted."*
-- Your frontend depends on different user roles (e.g., admin vs. regular user)
-- You need to simulate errors, timeouts, or permission checks
-- Backend state changes over time and should affect future responses (e.g., after a POST, the next GET reflects the update)
-- You need data that changes depending on query parameters or request bodies
-- You want to test workflows or sequences of API calls that depend on each other
-- You're working on features like pagination, filtering, or sorting
-- You need to simulate authentication and session-specific behavior
-- You want to create more realistic test scenarios for CI pipelines or manual testing
-- Your team needs fast feedback loops without relying on a fully working backend
+## Why Static Mocks Fall Short
-Dynamic mocks make your development process more reliable, realistic, and efficient.
-
-## What You Can Do with Mokapi Scripts
+Static responses are fine for trivial cases. But real development quickly surfaces their limits:
-- ✅ Return different responses based on query parameters, headers, or body content
-- ✅ Simulate authentication, authorization, and role-based access
-- ✅ Generate random, structured, or context-aware dynamic data
-- ✅ Mock complex workflows with conditional logic and stateful behavior
-- ✅ Chain requests together to simulate real-world usage patterns
-- ✅ Customize error responses, delays, and status codes
+- **Role-based responses**
+ Admin and regular users see different data. A static mock can only show one.
+- **Simulating errors & timeouts**
+ You need your frontend to handle 403s, 429s, and network failures, but your mock always returns 200.
+- **Stateful workflows**
+ After a POST, the next GET should reflect the change. Static mocks have no memory.
+- **Dynamic filtering & pagination**
+ Query parameters like `?page=2` or `?name=laptop` should produce meaningful results.
+- **Sequential request chains**
+ Login → fetch profile → update settings: static mocks can't model these flows.
+- **Auth & session behavior**
+ Missing or invalid tokens should behave differently from valid ones in your test environment.
-All using familiar JavaScript or TypeScript — no need to learn a new DSL.
+Dynamic mocks solve all of these. They make your development process more reliable, your test suites more
+realistic, and your feedback loops faster.
-## Example: Conditional Response Based on a Query Parameter
+## What You Can Do with Mokapi Scripts
-Let’s say your API returns a list of products. You want to simulate:
+- Return different responses based on query parameters, headers, or body content
+- Simulate authentication, authorization, and role-based access control
+- Generate random, structured, or context-aware dynamic data on the fly
+- Mock complex workflows with conditional logic and stateful behavior
+- Chain requests together to simulate real-world usage patterns
+- Customize error responses, status codes, and artificial delays
+- Mock an HTML login form to simulate an external identity provider
-- A search operation when a query parameter (name) is provided
-- An error response when the query parameter is exactly "error"
+## Example: Dynamic Product Search
-Here’s how easy it is with Mokapi Scripts:
+Let's build a realistic product list endpoint. It should:
+- Return the full product catalog when no filter is applied
+- Filter products by name when a ?name= query parameter is provided
+- Return a `400` error with a custom message when `?name=error` is passed
-```typescript
-import { on } from 'mokapi';
+```javascript title=products.js
+import { on } from 'mokapi'
const products = [
- { name: 'Laptop Pro 15' },
- { name: 'Wireless Mouse' },
- { name: 'Mechanical Keyboard' },
- { name: 'Noise Cancelling Headphones' },
- { name: '4K Monitor' },
- { name: 'USB-C Hub' }
-];
+ { name: 'Laptop Pro 15' },
+ { name: 'Wireless Mouse' },
+ { name: 'Mechanical Keyboard' },
+ { name: 'Noise Cancelling Headphones' },
+ { name: '4K Monitor' },
+ { name: 'USB-C Hub' }
+]
export default () => {
- on('http', (request, response): boolean => {
- if (request.query.name) {
- if (request.query.name === 'error') {
- response.body = 'A custom error message';
- response.statusCode = 400;
- } else {
- const matchingProducts = products.filter(p =>
- p.name.toLowerCase().includes(request.query.name.toLowerCase())
- );
- response.data = {products: matchingProducts};
- return true;
- }
- }
- return false;
- });
-}
-```
-
-### response.data vs. response.body
-In Mokapi, you control the response with either `response.data` or `response.body`:
-
-#### `response.data`
+ on('http', (request, response) => {
+ const nameFilter = request.query.name
-- Any JavaScript value (object, array, number, etc.)
-- Mokapi:
- - ✅ Validates it against your OpenAPI specification.
- - ✅ Converts it to the correct format (JSON, XML, etc.)
-
-Use this when you want automatic validation and formatting.
+ if (!nameFilter) {
+ // No filter — return everything
+ response.data = { products: products }
+ return
+ }
-#### `response.body`
-- Must be a string.
-- Mokapi:
- - ❌ Skips validation
- - ✅ Gives you full control (e.g., raw HTML, plain text)
+ if (nameFilter === 'error') {
+ // Simulate a validation error
+ response.rebuild(400)
+ response.data.message = 'A custom error message'
+ return
+ }
-Use this when you want to simulate freeform or invalid content.
+ // Filter products by name (case-insensitive)
+ const matched = products.filter(p =>
+ p.name.toLowerCase().includes(nameFilter.toLowerCase())
+ )
+
+ response.data = { products: matched }
+ })
+}
+```
## Use Cases
-### 1. Frontend Development
-
-Test UI flows with realistic behavior — pagination, filtering, auth, and more —
-without waiting for backend implementation.
-
-### 2. Testing and QA
-
-Simulate edge cases, failures, and timeouts directly in your mock server —
-ideal for automated or manual testing.
-
-### 3. Rapid Prototyping
-
-Show real-world behavior in your prototypes using dynamic data. Build better
-demos and get faster feedback.
+- **Frontend Development**
+ Test UI flows with realistic behavior—pagination, filtering, auth, and error states—without waiting for a working backend.
+- **Testing & QA**
+ Simulate edge cases, failures, and timeouts directly in your mock server. Ideal for automated CI pipelines and manual exploratory testing.
+- **Rapid Prototyping**
+ Show real-world behavior in demos and prototypes using dynamic data. Build faster, get feedback sooner.
## Getting Started
-1. Write your OpenAPI or AsyncAPI spec (or generate it)
-2. Add a Script where you control the response with JavaScript
-3. Run Mokapi — that’s it!
-
-👉 Try the [OpenAPI Mocking Tutorial](/resources/tutorials/get-started-with-rest-api) for a guided walkthrough.
-
-👉 Check out the [Mokapi Installation Guide](/docs/get-started/installation.md) to get set up in minutes.
-
-## Conclusion
-
-Static mocks are yesterday’s solution. \
-Mokapi Scripts bring a new level of control, flexibility, and realism to your API development process — all powered by JavaScript.
-
-No backend? No problem. \
-With Mokapi, your mocks behave exactly the way you need them to.
+1. **Write your specification**
+ Start with an OpenAPI spec, or generate one from an existing service. This defines the shape of your API.
+2. **Add a Mokapi Script**
+ Drop a JavaScript file next to your spec. Register event handlers that inspect the request and set the response however you need.
+3. **Run Mokapi**
+ That's it. Your dynamic mock is live, validated against your spec, and visible in the Mokapi dashboard.
## Further Reading
-- [Debugging Mokapi JavaScript](/resources/blogs/debugging-mokapi-scripts)\
- Learn how to debug your JavaScript code inside Mokapi
-- [End-to-End Testing with Mock APIs Using Mokapi](/resources/blogs/end-to-end-testing-with-mocked-apis)\
- Improve your end-to-end tests by mocking APIs with Mokapi.
+- [Debugging Mokapi JavaScript](/resources/blogs/debugging-mokapi-scripts)
+ Learn how to use `console.log`, `console.error`, and event handler tracing to see exactly what your scripts are doing.
+- [End-to-End Testing with Mock APIs Using Mokapi](/resources/blogs/end-to-end-testing-with-mocked-apis)
+ Improve your end-to-end test suites by replacing live backend dependencies with Mokapi.
---
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 317a440bf..9d5b6b993 100644
--- a/docs/resources/blogs/end-to-end-testing-mocked-apis.md
+++ b/docs/resources/blogs/end-to-end-testing-mocked-apis.md
@@ -1,44 +1,60 @@
---
-title: End-to-End Testing with Mock APIs Using Mokapi
-description: Improve your end-to-end tests by mocking APIs with Mokapi. Integrate it into your CI/CD pipeline for faster, more reliable testing.
+title: End-to-End Testing with Mocked APIs
+description: Stop relying on flaky external services. Build faster, more reliable test pipelines with Mokapi-powered API mocks in your CI/CD.
+subtitle: Stop relying on flaky external services. Build faster, more reliable test pipelines with Mokapi-powered API mocks in your CI/CD.
---
-# End-to-End Testing with Mocked APIs Using Mokapi
+# End-to-End Testing with Mocked APIs
-Building reliable applications often depends on being able to test real-world scenarios — including how your app
-behaves when communicating with external APIs. However, relying on real APIs during development and CI/CD
-pipelines can introduce problems like slow test runs, flaky results, or even test failures when external
-services are unavailable.
+Building reliable applications depends on being able to test real-world scenarios, including how your app
+behaves when communicating with external APIs. But relying on live APIs during development and CI/CD
+pipelines introduces problems: slow test runs, flaky results, and failures when external services are unavailable.
-That’s where mocking APIs comes in.
+That's where API mocking comes in and specifically, where Mokapi transforms your testing workflow.
-## Why Mock APIs for Testing?
+> With Mokapi, you can simulate external systems under controlled conditions using OpenAPI
+> or AsyncAPI specifications. The result? Faster feedback, fewer bugs, and tests you can actually trust.
-Mocking APIs allows you to simulate external systems under controlled conditions. This means:
+## Why Mock APIs for Testing?
-- ⚡ Faster and more reliable test runs
-- 🎯 Full control over the data and behavior returned by APIs
-- 🛠️ Easier testing of error scenarios, timeouts, and edge cases
-- 🔗 No dependencies on the availability of third-party services
+Mocking APIs gives you control over the testing environment in ways that real external services simply can't provide:
-With [Mokapi](https://mokapi.io), you can easily define API mocks using [OpenAPI](/docs/http/overview.md) or
-[AsyncAPI](/docs/kafka/overview.md) specifications, and serve them locally or in your CI environment.
-Mokapi even supports dynamic behavior using simple [JavaScripts](/docs/javascript-api/overview.md), helping you create more realistic test scenarios.
+``` box=benefits title="Faster Test Runs"
+No network latency, no waiting for slow third-party responses. Tests complete in seconds, not minutes.
+```
-## How Mokapi Fits into Your CI/CD Pipeline
+``` box=benefits title="Full Control"
+Return exactly the data you need. Simulate specific states, edge cases, and user scenarios on demand.
+```
-Running Mokapi during your automated tests helps isolate your systems and catch integration bugs earlier.
-Here's what a typical flow looks like:
+``` box=benefits title="Test Error Scenarios"
+Force 500 errors, timeouts, malformed responses, and rate limits without breaking real services.
+```
-```shell
-Code Push → Start Mokapi → Run Tests Against Mocks → Stop Mokapi
+``` box=benefits title="Zero Dependencies"
+Tests run offline, in CI, or on developer machines—no reliance on third-party availability or credentials.
```
-You can even run Mokapi in your GitHub Actions workflows, making your CI/CD pipelines faster and more predictable.
+
+Mokapi makes this straightforward: define your API contracts with OpenAPI or AsyncAPI specifications,
+add optional JavaScript for dynamic behavior, and you're done. Your mocks are validated, versioned,
+and ready to serve.
+
+## How Mokapi Fits Into Your CI/CD Pipeline
+
+Integrating Mokapi into your automated test pipeline isolates your system under test
+and catches integration bugs early. The workflow is simple:
+
+
+
+Mokapi runs as a service during your test phase whether in Docker, Kubernetes, or directly on the CI runner.
+Your tests hit the mocked endpoints instead of real APIs, and Mokapi validates every response against your
+specifications.
## Example: GitHub Actions Workflow with Mokapi
-Here's a basic example of using Mokapi in GitHub Actions:
-```yaml
+Here's a practical example showing how to integrate Mokapi into a GitHub Actions workflow for a Node.js backend:
+
+```yaml tab=.github/workflows/test.yml
name: Node.js Backend Tests
on:
@@ -58,7 +74,10 @@ jobs:
- name: Start Mokapi
run: |
- docker run -d --rm --name mokapi -p 80:80 -p 8080:8080 -v ${{ github.workspace }}/mocks:/mocks mokapi/mokapi:latest /mocks
+ docker run -d --name mokapi \
+ -p 80:80 \
+ -v ${{ github.workspace }}/mocks:/mocks \
+ mokapi/mokapi:latest /mocks
sleep 5 # Ensure Mokapi is running
- name: Set Up Node.js
@@ -76,33 +95,68 @@ jobs:
run: docker stop mokapi
```
-In this setup, Mokapi runs as in a Docker container in GitHub Actions, making your mocked APIs available for the duration of your tests.
+### What's Happening Here?
+
+- **Checkout:** The workflow pulls your code, including your mock definitions stored in `/mocks`
+- **Start Mokapi:** A Docker container runs Mokapi, mounting your mock specifications and exposing port 80 (API)
+- **Set Up Node.js:** The test environment is configured with Node.js 20
+- **Install & Test:** Dependencies are installed, and the test suite runs, hitting Mokapi instead of real APIs
+- **Cleanup:** Mokapi stops automatically, keeping the CI environment clean
+
+## Advanced Mocking with Mokapi
-## Beyond the Basics: Advanced Mocking with Mokapi
+Basic mocking gets you far, but Mokapi's advanced features let you simulate complex real-world scenarios:
-Want even more control? Mokapi supports advanced mocking features, such as:
+``` box=feature title="Simulating Timeouts"
+Test how your app handles slow APIs by adding artificial delays to responses
+```
-- **Simulating Timeouts** — Test how your app handles slow APIs
-- **Dynamic Responses** — Return different data based on query parameters or headers
-- **Error Scenarios** — Force 500 errors or custom error messages to test error handling
+``` box=feature title="Dynamic Responses"
+Return different data based on query parameters, headers, or request body content
+```
-You can even modify mocks dynamically during a test run with simple [JavaScripts](/docs/javascript-api/overview.md)!
+``` box=feature title="Error Scenarios"
+Force 500 errors, 429 rate limits, or custom error messages to validate error handling
+```
-## Local Development and Mokapi
+These capabilities are powered by Mokapi Scripts, lightweight JavaScript modules that let you
+define custom behavior. Here's a quick example for delay simulation:
+
+```javascript tab=simulations.js
+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;
+ }
+ }
+ })
+ on('http', (request, response) => {
+ if (!delay) {
+ sleep(delay);
+ }
+ }, { track: true } )
+}
+```
-Mokapi isn't just for CI. You can also run it locally during development.
-There are many ways to run Mokapi depending on your setup — learn more in the [Running Mokapi Guide](/docs/get-started/running.md).
+This script demonstrates how to use Mokapi Scripts to dynamically simulate response delays at runtime using HTTP requests.
-## Conclusion
+It allows you to enable, change, or disable artificial latency without restarting Mokapi.
-Mocking APIs is a crucial part of building robust, scalable systems. With Mokapi, you can easily integrate mocking into your local
-development and CI pipelines, leading to faster feedback, fewer bugs, and better products.
+## Ready to ship faster, more reliable software?
-**Ready to ship faster, more reliable software?**
-[Get started with Mokapi](/docs/get-started/installation.md)
+- [Get started with Mokapi](/docs/get-started/installation.md)
+- [Full GitHub Actions Tutorial](/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline).
-For a detailed, step-by-step guide on how to use Mokapi in your GitHub Actions workflows,
-see the [GitHub Actions and Mokapi](/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline).
---
diff --git a/docs/resources/tutorials/simple-http-api.md b/docs/resources/tutorials/simple-http-api.md
index 8bb380e9b..60013531e 100644
--- a/docs/resources/tutorials/simple-http-api.md
+++ b/docs/resources/tutorials/simple-http-api.md
@@ -1,18 +1,42 @@
---
title: Get started with REST API
-description: This tutorial will show you how to mock a REST API using an OpenAPI specification.
+description: Learn how to mock a REST API using an OpenAPI specification with Mokapi. This tutorial covers basic setup, configuration, and Docker deployment.
+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
+cards:
+ items:
+ - title: Dynamic Responses
+ href: /resources/blogs/bring-your-mock-apis-to-life-with-javascript
+ description: Return different data based on query parameters, headers, or request body content
+ - title: Debugging
+ href: /resources/blogs/debugging-mokapi-scripts
+ description: Learn how to debug your JavaScript code with console logging and event tracing
+ - 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 learn how to mock an OpenAPI specification with Mokapi, how to configure Mokapi,
-and how to run it in a Docker container. In further examples we will see how the test quality can be improved with some
-edge cases such as increased response time or response errors like internal server errors.
+In this tutorial, you will:
-## Create OpenAPI file
+- Create an OpenAPI specification defining a REST API endpoint
+- Write a JavaScript script to generate dynamic mock data
+- Configure and run Mokapi in a Docker container
+- Test the mocked API with HTTP requests
-First, we create an OpenAPI specification file `users.yaml` that defines a /users endpoint
+By the end of this tutorial, you'll have a working REST API mock that returns dynamically generated user data.
+
+``` box=tree title="Project Structure"
+📄 users.yaml
+📄 users.js
+📄 Dockerfile
+```
+
+## Create OpenAPI file
+
+Create a file named `users.yaml` that defines a `/users` endpoint:
```yaml
openapi: 3.0.0
@@ -35,10 +59,16 @@ paths:
items:
type: string
```
-This specification defines an endpoint /api/users that returns an array of strings containing usernames.
-## Create Mokapi Scripts
-Next, create a JavaScript file `users.js` which sets the content of the response
+``` box=info title="What This Specification Does"
+This OpenAPI specification defines a single endpoint GET /api/users that returns a JSON array
+of strings representing usernames. The specification serves as both documentation and a contract
+ for the mock server.
+```
+
+## Create Mokapi Script
+
+Create a JavaScript file named `users.js` to generate dynamic mock data:
```javascript
import { on } from 'mokapi'
@@ -56,10 +86,23 @@ export default function() {
})
}
```
-The script sets a string array containing three random usernames to the response data if the requested endpoint is `/api/users`.
-## Create a Dockerfile
-Next create a `Dockerfile` to configure Mokapi
+### How This Script Works
+- **Event Listener:** The `on('http', ...)` function registers a handler that fires for every HTTP request
+- **Path Matching:** The script checks if the request path matches `/api/users`
+- **Dynamic Data:** The `fake()` function generates random usernames using Mokapi's built-in Faker library
+- **Response Data:** Setting `response.data` tells Mokapi to use this data as response body
+
+``` box=info title="Why Use response.data?"
+Using response.data instead of response.body ensures Mokapi validates your mock data against
+the OpenAPI specification. This catches schema mismatches early and guarantees your mocks
+match your API contract.
+```
+
+## Create Dockerfile
+
+Create a `Dockerfile` to configure and package Mokapi with your specification files:
+
```dockerfile
FROM mokapi/mokapi:latest
@@ -70,17 +113,48 @@ COPY ./users.js /demo/
CMD ["--Providers.File.Directory=/demo"]
```
-## Start Mokapi
+### Dockerfile Breakdown
+
+- **Base Image:** Starts from the official Mokapi Docker image
+- **Copy Files:** Copies both the OpenAPI specification and JavaScript script into the `/demo` directory
+- **Configuration:** The `CMD` instruction tells Mokapi to load specifications from `/demo`
+
+## Start Mokapi
+
+Build and run the Docker container in one command:
```
docker run -p 80:80 -p 8080:8080 --rm -it $(docker build -q .)
```
-You can open a browser and go to Mokapi's Dashboard (http://localhost:8080) to see the Mokapi's HTTP REST API.
-## Make HTTP request
-You can now make HTTP requests to our Sample API and Mokapi creates responses with randomly generated data.
+This command:
+- **Builds** the Docker image from your Dockerfile
+- **Exposes** port 80 for the mock REST API
+- **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 REST API. The dashboard shows all configured endpoints, recent requests, and response details.
+```
+
+## Make HTTP request
+
+Test your mocked API by making an HTTP request to the `/api/users` endpoint:
```
curl --header "Accept: application/json" http://localhost/api/users
```
-The request and the response are visible in the dashboard.
\ No newline at end of file
+
+``` box=info title="View in Dashboard"
+Every request and response is visible in the Mokapi dashboard at http://localhost:8080. You can
+inspect headers, request bodies, response times, and see which event handlers were triggered.
+```
+
+Each time you make a request, Mokapi generates three new random usernames. The response is validated
+against your OpenAPI specification to ensure it matches the defined schema.
+
+## Next Steps
+
+Now that you have a working REST API mock, explore these advanced topics:
+
+{{ card-grid key="cards" }}
\ No newline at end of file
diff --git a/js/mokapi/proxy.go b/js/mokapi/proxy.go
index 374343020..065ca3bd9 100644
--- a/js/mokapi/proxy.go
+++ b/js/mokapi/proxy.go
@@ -304,6 +304,10 @@ func splice(slice, toAdd reflect.Value, start int, deleteCount int) {
}
func assignValue(field reflect.Value, value any, fieldName string) error {
+ if value == nil {
+ field.Set(reflect.Zero(field.Type()))
+ return nil
+ }
v, err := convertTo(field.Type(), reflect.ValueOf(value))
if err != nil {
return fmt.Errorf("failed to set %s: %w", fieldName, err)
diff --git a/js/mokapi/shared.go b/js/mokapi/shared.go
index 3a9f65844..80cae9196 100644
--- a/js/mokapi/shared.go
+++ b/js/mokapi/shared.go
@@ -86,7 +86,7 @@ func Export(v any) any {
case *Proxy:
return val.Export()
case *SharedValue:
- return val.source.Export()
+ return Export(val.source)
case goja.Value:
return Export(val.Export())
default:
@@ -136,8 +136,12 @@ func (p *SharedValue) Get(key string) goja.Value {
f := v.Get(key)
if _, ok := goja.AssertFunction(f); ok {
return f
- } else if _, isObject := f.(*goja.Object); isObject {
- return p.vm.NewDynamicObject(NewSharedValue(f, p.vm))
+ } else if obj, isObject := f.(*goja.Object); isObject {
+ e := obj.Export()
+ if sv, ok := e.(*SharedValue); ok {
+ return sv.Use(p.vm).ToValue()
+ }
+ return f
}
return f
}
@@ -157,7 +161,8 @@ func (p *SharedValue) Has(key string) bool {
func (p *SharedValue) Set(key string, value goja.Value) bool {
switch v := p.source.(type) {
case *goja.Object:
- err := v.Set(key, value)
+ sv := useValue(value, p.vm)
+ err := v.Set(key, sv)
if err != nil {
panic(p.vm.ToValue(err))
}
@@ -205,3 +210,12 @@ func (p *SharedValue) ToValue() goja.Value {
func (p *SharedValue) Export() any {
return p.source.Export()
}
+
+func useValue(v goja.Value, vm *goja.Runtime) any {
+ switch v.(type) {
+ case *goja.Object:
+ return NewSharedValue(v, vm)
+ default:
+ return v
+ }
+}
diff --git a/js/mokapi/shared_test.go b/js/mokapi/shared_test.go
index 385cee25d..70f80b246 100644
--- a/js/mokapi/shared_test.go
+++ b/js/mokapi/shared_test.go
@@ -259,6 +259,57 @@ func TestModule_Shared(t *testing.T) {
r.Equal(t, map[string]any{"items": []any{int64(123)}}, mokapi.Export(v))
},
},
+ {
+ name: "update object inside array",
+ test: func(t *testing.T, newVm func() *goja.Runtime) {
+ vm1 := newVm()
+ vm2 := newVm()
+
+ v, err := vm1.RunString(`
+ const m = require('mokapi');
+ const foo = m.shared.update('foo', (v) => v ?? { items: [] });
+ foo.items.push({ data: 'foo' })
+ `)
+ r.NoError(t, err)
+
+ v, err = vm2.RunString(`
+ const m = require('mokapi');
+ const foo = m.shared.update('foo', (v) => v ?? { items: [] });
+ foo.items.push({ data: 'bar' })
+ foo
+ `)
+ r.NoError(t, err)
+ m := map[string]interface{}{}
+ err = vm1.ExportTo(v, &m)
+ r.Equal(t, map[string]any{"items": []any{map[string]any{"data": "foo"}, map[string]any{"data": "bar"}}}, mokapi.Export(v))
+ },
+ },
+ {
+ name: "change object inside array",
+ test: func(t *testing.T, newVm func() *goja.Runtime) {
+ vm1 := newVm()
+ vm2 := newVm()
+
+ v, err := vm1.RunString(`
+ const m = require('mokapi');
+ const foo = m.shared.update('foo', (v) => v ?? { items: [] });
+ foo.items.push({ data: 'foo' })
+ `)
+ r.NoError(t, err)
+
+ v, err = vm2.RunString(`
+ const m = require('mokapi');
+ const foo = m.shared.update('foo', (v) => v ?? { items: [] });
+ foo.items.push({ data: 'bar' })
+ foo.items[1].data = 'yuh'
+ foo
+ `)
+ r.NoError(t, err)
+ m := map[string]interface{}{}
+ err = vm1.ExportTo(v, &m)
+ r.Equal(t, map[string]any{"items": []any{map[string]any{"data": "foo"}, map[string]any{"data": "yuh"}}}, mokapi.Export(v))
+ },
+ },
{
name: "enumerate object",
test: func(t *testing.T, newVm func() *goja.Runtime) {
diff --git a/providers/mail/log.go b/providers/mail/log.go
index 51cb33e33..df25b6e2f 100644
--- a/providers/mail/log.go
+++ b/providers/mail/log.go
@@ -5,6 +5,8 @@ import (
"mokapi/engine/common"
"mokapi/runtime/events"
"mokapi/smtp"
+
+ log "github.com/sirupsen/logrus"
)
type Log struct {
@@ -27,7 +29,13 @@ func NewLogEvent(msg *smtp.Message, ctx *smtp.ClientContext, eh events.Handler,
if msg != nil {
event.MessageId = msg.MessageId
- event.Subject = msg.Subject
+ subject, err := smtp.DecodeHeaderValue(msg.Subject)
+ if err != nil {
+ log.Errorf("failed to decode subject: %v", err)
+ event.Subject = msg.Subject
+ } else {
+ event.Subject = subject
+ }
}
_ = eh.Push(event, traits.WithNamespace("mail"))
diff --git a/providers/mail/smtp_handler_test.go b/providers/mail/smtp_handler_test.go
index bfa0f14a0..c412e781f 100644
--- a/providers/mail/smtp_handler_test.go
+++ b/providers/mail/smtp_handler_test.go
@@ -5,7 +5,7 @@ import (
"encoding/base64"
"mokapi/engine/enginetest"
"mokapi/providers/mail"
- "mokapi/runtime/events/eventstest"
+ "mokapi/runtime/events"
"mokapi/smtp"
"mokapi/smtp/smtptest"
"testing"
@@ -18,12 +18,12 @@ func TestHandler_ServeSMTP(t *testing.T) {
testcases := []struct {
name string
config *mail.Config
- test func(t *testing.T, h *mail.Handler, s *mail.Store, eh *eventstest.Handler)
+ test func(t *testing.T, h *mail.Handler, s *mail.Store, eh events.Handler)
}{
{
name: "no auth required",
config: &mail.Config{},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -39,7 +39,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, &smtp.AuthRequired, r.Result)
@@ -52,7 +52,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"alice@foo.bar": {Username: "alice", Password: "foo"},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendLogin(t, h, ctx, "foo", "foo")
require.Equal(t, &smtp.InvalidAuthCredentials, r.Result)
@@ -65,7 +65,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"alice@foo.bar": {Username: "alice", Password: "foo"},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendLogin(t, h, ctx, "alice", "foo")
require.Equal(t, &smtp.InvalidAuthCredentials, r.Result)
@@ -79,7 +79,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
},
Settings: &mail.Settings{AllowUnknownSenders: false},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
exp := smtp.AddressRejected
@@ -95,7 +95,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
},
Settings: &mail.Settings{AllowUnknownSenders: true},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
exp := smtp.Ok
@@ -110,7 +110,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"alice@foo.bar": {},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -119,7 +119,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "mail any is valid",
config: &mail.Config{},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -133,7 +133,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"alice@foo.bar": {},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
exp := smtp.AddressRejected
@@ -149,7 +149,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"bob@foo.bar": {},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -158,7 +158,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "rcpt any is valid",
config: &mail.Config{Settings: &mail.Settings{AutoCreateMailbox: true}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -167,7 +167,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "no rcpt is valid",
config: &mail.Config{Settings: &mail.Settings{AutoCreateMailbox: false}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.AddressRejected.StatusCode, r.Result.StatusCode)
@@ -183,7 +183,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
"rcpt": {Recipient: mail.NewRuleExpr("^support"), Action: mail.Allow},
},
},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.AddressRejected.StatusCode, r.Result.StatusCode)
@@ -194,7 +194,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "max recipients valid",
config: &mail.Config{Settings: &mail.Settings{AutoCreateMailbox: true, MaxRecipients: 5}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -207,7 +207,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "max recipients not valid",
config: &mail.Config{Settings: &mail.Settings{AutoCreateMailbox: true, MaxRecipients: 2}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendRcpt(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -222,18 +222,19 @@ func TestHandler_ServeSMTP(t *testing.T) {
{
name: "data",
config: &mail.Config{Info: mail.Info{Name: "Testing Mail Server"}, Settings: &mail.Settings{AutoCreateMailbox: true}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, eh *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, eh events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendData(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
- require.Len(t, eh.Events, 1)
- require.Equal(t, "namespace=mail, name=Testing Mail Server", eh.Events[0].Traits.String())
+ logs := eh.GetEvents(events.NewTraits().WithNamespace("mail"))
+ require.Len(t, logs, 1)
+ require.Equal(t, "namespace=mail, name=Testing Mail Server", logs[0].Traits.String())
},
},
{
name: "server should add message into mailbox",
config: &mail.Config{Settings: &mail.Settings{AutoCreateMailbox: true}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendData(t, h, ctx)
require.Equal(t, smtp.Ok, r.Result)
@@ -249,7 +250,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Sender: mail.NewRuleExpr(".*@mokapi.io"),
Action: mail.Allow,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, "Sender alice@foo.bar does not match allow rule: .*@mokapi.io", r.Result.Message)
@@ -262,7 +263,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Sender: mail.NewRuleExpr("@foo.bar"),
Action: mail.Deny,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, "Sender alice@foo.bar does match deny rule: @foo.bar", r.Result.Message)
@@ -282,7 +283,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Message: "custom error message",
},
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMail(t, h, ctx)
require.Equal(t, "custom error message", r.Result.Message)
@@ -297,7 +298,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Subject: mail.NewRuleExpr("^Hello"),
Action: mail.Deny,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMailWithSubjectAndBody(t, h, "Hello World", "", ctx)
require.Equal(t, "Subject Hello World does match deny rule: ^Hello", r.Result.Message)
@@ -312,7 +313,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Subject: mail.NewRuleExpr("^Hello"),
Action: mail.Allow,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMailWithSubjectAndBody(t, h, "foo", "", ctx)
require.Equal(t, "Subject foo does not match allow rule: ^Hello", r.Result.Message)
@@ -327,7 +328,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Body: mail.NewRuleExpr("^Hello"),
Action: mail.Deny,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMailWithSubjectAndBody(t, h, "Hello World", "Hello", ctx)
require.Equal(t, "Body Hello does match deny rule: ^Hello", r.Result.Message)
@@ -342,7 +343,7 @@ func TestHandler_ServeSMTP(t *testing.T) {
Body: mail.NewRuleExpr("^Hello"),
Action: mail.Allow,
}}},
- test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ *eventstest.Handler) {
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, _ events.Handler) {
ctx := smtp.NewClientContext(context.Background(), "")
r := sendMailWithSubjectAndBody(t, h, "Hello World", "foo", ctx)
require.Equal(t, "Body foo does not match allow rule: ^Hello", r.Result.Message)
@@ -350,15 +351,46 @@ func TestHandler_ServeSMTP(t *testing.T) {
require.Equal(t, smtp.EnhancedStatusCode{5, 7, 1}, r.Result.EnhancedStatusCode)
},
},
+ {
+ name: "log has subject",
+ config: &mail.Config{
+ Mailboxes: map[string]*mail.MailboxConfig{
+ "alice@foo.bar": {},
+ },
+ },
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, eh events.Handler) {
+ ctx := smtp.NewClientContext(context.Background(), "")
+ sendMailWithSubjectAndBody(t, h, "foo", "", ctx)
+ logs := eh.GetEvents(events.NewTraits().WithNamespace("mail"))
+ require.Len(t, logs, 1)
+ require.Equal(t, "foo", logs[0].Data.(*mail.Log).Subject)
+ },
+ },
+ {
+ name: "encoded subject",
+ config: &mail.Config{
+ Mailboxes: map[string]*mail.MailboxConfig{
+ "alice@foo.bar": {},
+ },
+ },
+ test: func(t *testing.T, h *mail.Handler, s *mail.Store, eh events.Handler) {
+ ctx := smtp.NewClientContext(context.Background(), "")
+ sendMailWithSubjectAndBody(t, h, "=?UTF-8?Q?=C2=A1Buenos_d=C3=ADas!?=", "", ctx)
+ logs := eh.GetEvents(events.NewTraits().WithNamespace("mail"))
+ require.Len(t, logs, 1)
+ require.Equal(t, "¡Buenos días!", logs[0].Data.(*mail.Log).Subject)
+ },
+ },
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
s := mail.NewStore(tc.config)
- eh := &eventstest.Handler{}
- h := mail.NewHandler(tc.config, s, enginetest.NewEngine(), eh)
- tc.test(t, h, s, eh)
+ sm := events.NewStoreManager(nil)
+ sm.SetStore(10, events.NewTraits().WithNamespace("mail"))
+ h := mail.NewHandler(tc.config, s, enginetest.NewEngine(), sm)
+ tc.test(t, h, s, sm)
})
}
}
diff --git a/smtp/message.go b/smtp/message.go
index b5c2841c4..6d9b12210 100644
--- a/smtp/message.go
+++ b/smtp/message.go
@@ -5,8 +5,6 @@ import (
"bytes"
"encoding/base64"
"fmt"
- "github.com/google/uuid"
- log "github.com/sirupsen/logrus"
"io"
"mime"
"mime/multipart"
@@ -18,6 +16,9 @@ import (
"os"
"strings"
"time"
+
+ "github.com/google/uuid"
+ log "github.com/sirupsen/logrus"
)
const DateTimeLayout = "02-Jan-2006 15:04:05 -0700"
@@ -504,3 +505,12 @@ func (a *Attachment) Headers() map[string]string {
}
return headers
}
+
+func DecodeHeaderValue(s string) (string, error) {
+ dec := new(mime.WordDecoder)
+ r, err := dec.DecodeHeader(s)
+ if err != nil {
+ return s, fmt.Errorf("failed to decode SMTP header: %v", err)
+ }
+ return r, nil
+}
diff --git a/smtp/message_test.go b/smtp/message_test.go
new file mode 100644
index 000000000..04d31f73e
--- /dev/null
+++ b/smtp/message_test.go
@@ -0,0 +1,43 @@
+package smtp_test
+
+import (
+ "mokapi/smtp"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeHeaderValue(t *testing.T) {
+ testcases := []struct {
+ name string
+ in string
+ want string
+ }{
+ {
+ name: "empty",
+ in: "",
+ want: "",
+ },
+ {
+ name: "utf-8",
+ in: "=?UTF-8?Q?=C2=A1Buenos_d=C3=ADas!?=",
+ want: "¡Buenos días!",
+ },
+ {
+ name: "base64",
+ in: "=?UTF-8?B?bW9rYXBp?=",
+ want: "mokapi",
+ },
+ }
+
+ t.Parallel()
+ for _, tc := range testcases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ act, err := smtp.DecodeHeaderValue(tc.in)
+ require.NoError(t, err)
+ require.Equal(t, tc.want, act)
+ })
+ }
+}
diff --git a/webui/public/ci-pipeline-flow.png b/webui/public/ci-pipeline-flow.png
new file mode 100644
index 000000000..cc812851b
Binary files /dev/null and b/webui/public/ci-pipeline-flow.png differ
diff --git a/webui/public/javascript-debug-event-handler.png b/webui/public/javascript-debug-event-handler.png
new file mode 100644
index 000000000..fb417689b
Binary files /dev/null and b/webui/public/javascript-debug-event-handler.png differ
diff --git a/webui/src/assets/home.css b/webui/src/assets/home.css
index 9a7216219..52874c7bd 100644
--- a/webui/src/assets/home.css
+++ b/webui/src/assets/home.css
@@ -93,10 +93,10 @@
border-color: var(--card-border);
background-color: var(--card-background);
}
-[data-theme="light"] .home .card:not(.accented) {
+[data-theme="light"] .card:not(.accented) {
border: solid 1px var(--card-border) !important;
}
-.home .card.accented {
+.card.accented {
background-color: transparent;
box-shadow: none !important;
}
@@ -124,24 +124,24 @@
[data-theme="light"] .card.accented::before {
border: solid 1px var(--card-border) !important;
}
-.home .card.accented .cta {
+.card.accented .cta {
color: var(--link-color);
}
-.home .card:hover.accented .cta {
+.card:hover.accented .cta {
color: var(--color-text);
}
-.home .card.accented .cta span.bi {
+.card.accented .cta span.bi {
font-size: 12px;
position: relative;
bottom:2px;
}
-.home .card.accented .cta span.hover {
+.card.accented .cta span.hover {
display: none;
}
-.home .card:hover.accented .cta span.hover {
+.card:hover.accented .cta span.hover {
display: inline;
}
-.home .card:hover.accented .cta span:not(.hover) {
+.card:hover.accented .cta span:not(.hover) {
display: none;
}
diff --git a/webui/src/assets/vars.css b/webui/src/assets/vars.css
index 55b16509f..5f6e746e1 100644
--- a/webui/src/assets/vars.css
+++ b/webui/src/assets/vars.css
@@ -134,7 +134,7 @@
--color-doc-text-hover: rgb(8, 109, 215);
--color-doc-border-hover: rgb(22, 19, 21);
- --color-button-link: rgb(2, 101, 210);
+ --color-button-link: rgb(8, 109, 215);
--color-button-link-active: rgb(8, 109, 215);
--color-button-link-inactive: rgb(101, 109, 118);
--color-button-text-hover: #fff;
diff --git a/webui/src/composables/file-resolver.ts b/webui/src/composables/file-resolver.ts
index e016807a2..58510ef1e 100644
--- a/webui/src/composables/file-resolver.ts
+++ b/webui/src/composables/file-resolver.ts
@@ -3,10 +3,7 @@ import type { RouteLocationNormalizedLoaded } from "vue-router"
export function useFileResolver() {
function resolve(config: DocConfig, route: RouteLocationNormalizedLoaded): DocEntry | undefined {
- let path = route.path
- if (path.endsWith('/')) {
- path = path.substring(0, path.length-1)
- }
+ const path = normalizePath(route.path)
for (const name of Object.keys(config)) {
const entry = getEntries(config[name]!, (e) => e.path?.toLocaleLowerCase() === path)
if (entry) {
@@ -34,8 +31,9 @@ export function useFileResolver() {
}
function getBreadcrumb(config: DocConfig, route: RouteLocationNormalizedLoaded): DocEntry[] | undefined {
+ const path = normalizePath(route.path)
for (const name of Object.keys(config)) {
- const entries = getEntries(config[name]!, (e) => e.path === route.path)
+ const entries = getEntries(config[name]!, (e) => e.path === path)
if (entries) {
entries[0] = Object.assign({ label: name }, entries[0])
return entries
@@ -56,5 +54,12 @@ export function useFileResolver() {
return undefined
}
+ function normalizePath(path: string): string {
+ if (path.endsWith('/')) {
+ path = path.substring(0, path.length-1)
+ }
+ return path
+ }
+
return { resolve, getBreadcrumb, getEntryBySource }
}
\ No newline at end of file
diff --git a/webui/src/composables/markdown-box.ts b/webui/src/composables/markdown-box.ts
index fcfe6f93f..f83c896c4 100644
--- a/webui/src/composables/markdown-box.ts
+++ b/webui/src/composables/markdown-box.ts
@@ -90,7 +90,7 @@ export function MarkdownItBox(md: MarkdownIt, opts: Options) {
token.hidden = true
const url = getUrl(token)
- const content = parseContent(token.content)
+ let content = parseContent(token.content)
if (showTitle(token)) {
let title = getTitle(token)
@@ -112,25 +112,54 @@ export function MarkdownItBox(md: MarkdownIt, opts: Options) {
case 'info':
icon = ''
break
+ case 'result':
+ icon = ''
+ break
+ case 'tree':
+ content = parseTree(content)
+ break
}
- alert += `
${icon}${title}
-${content}
+${content}
${url}