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 -Example JavaScript code for Mokapi Scripts with annotated benefits of using dynamic API mocks +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: + +Typical CI/CD Flow with Mokapi + +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 += `