From 2cf2198b3ddbd0146b3c0252844e1f9e88743b6f Mon Sep 17 00:00:00 2001
From: marle3003
Date: Thu, 5 Feb 2026 18:25:19 +0100
Subject: [PATCH 01/75] add CLI short parameters -v and -h fix: cleanup GIT
repository on exit update welcome documentation
---
cmd/mokapi/main_test.go | 21 +++++++++++
config/dynamic/provider/git/git.go | 14 ++++----
config/dynamic/provider/git/git_test.go | 48 +++++++++++++++++++++++--
docs/guides/get-started/welcome.md | 10 ++++++
pkg/cli/command.go | 4 +--
webui/src/assets/vars.css | 6 ++++
webui/src/views/DocsView.vue | 4 +--
7 files changed, 94 insertions(+), 13 deletions(-)
diff --git a/cmd/mokapi/main_test.go b/cmd/mokapi/main_test.go
index 08e8384b0..37e75f65a 100644
--- a/cmd/mokapi/main_test.go
+++ b/cmd/mokapi/main_test.go
@@ -23,6 +23,27 @@ func TestMain_Skeleton(t *testing.T) {
require.Equal(t, "1.0\n", out)
},
},
+ {
+ name: "version short",
+ args: []string{"-v"},
+ test: func(t *testing.T, out string) {
+ require.Equal(t, "1.0\n", out)
+ },
+ },
+ {
+ name: "help",
+ args: []string{"--help"},
+ test: func(t *testing.T, out string) {
+ require.Contains(t, out, "Mokapi is an easy, modern and flexible API mocking tool using Go")
+ },
+ },
+ {
+ name: "help short",
+ args: []string{"-h"},
+ test: func(t *testing.T, out string) {
+ require.Contains(t, out, "Mokapi is an easy, modern and flexible API mocking tool using Go")
+ },
+ },
{
name: "generate-cli-skeleton",
args: []string{"--generate-cli-skeleton"},
diff --git a/config/dynamic/provider/git/git.go b/config/dynamic/provider/git/git.go
index 6474d7d90..8bec16815 100644
--- a/config/dynamic/provider/git/git.go
+++ b/config/dynamic/provider/git/git.go
@@ -140,17 +140,20 @@ func (p *Provider) initRepository(r *repository, ch chan dynamic.ConfigEvent, po
r.repo, err = git.PlainClone(r.localPath, false, options)
if err != nil {
+ p.cleanup(r)
return fmt.Errorf("unable to clone git %q: %v", r.repoUrl, err)
}
r.wt, err = r.repo.Worktree()
if err != nil {
+ p.cleanup(r)
return fmt.Errorf("unable to get git worktree: %v", err.Error())
}
r.pullOptions = &git.PullOptions{SingleBranch: true, Depth: 1}
ref, err := r.repo.Head()
if err != nil {
+ p.cleanup(r)
return fmt.Errorf("unable to get git head: %w", err)
}
r.hash = ref.Hash()
@@ -166,6 +169,7 @@ func (p *Provider) initRepository(r *repository, ch chan dynamic.ConfigEvent, po
for {
select {
case <-ctx.Done():
+ p.cleanup(r)
return
case e := <-chFile:
path := e.Name
@@ -196,12 +200,10 @@ func (p *Provider) startFileProvider(dir string, ch chan dynamic.ConfigEvent, po
}
}
-func (p *Provider) cleanup() {
- for _, repo := range p.repositories {
- err := os.RemoveAll(repo.localPath)
- if err != nil {
- log.Debugf("unable to remove temp dir %q: %v", repo.localPath, err.Error())
- }
+func (p *Provider) cleanup(r *repository) {
+ err := os.RemoveAll(r.localPath)
+ if err != nil {
+ log.Debugf("unable to remove temp dir %q: %v", r.localPath, err.Error())
}
}
diff --git a/config/dynamic/provider/git/git_test.go b/config/dynamic/provider/git/git_test.go
index 5fba0a36a..2484190f9 100644
--- a/config/dynamic/provider/git/git_test.go
+++ b/config/dynamic/provider/git/git_test.go
@@ -2,9 +2,6 @@ package git
import (
"context"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/stretchr/testify/require"
"mokapi/config/dynamic"
"mokapi/config/static"
"mokapi/safe"
@@ -14,6 +11,10 @@ import (
"strings"
"testing"
"time"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/stretchr/testify/require"
)
var gitFiles = map[string]struct{}{
@@ -225,6 +226,33 @@ Stop:
require.Len(t, files, 1)
}
+func TestGitFileUpdate(t *testing.T) {
+ repo := newGitRepo(t, t.Name())
+ repo.commit(t, "foo.txt", "foo")
+
+ g := New(static.GitProvider{Urls: []string{repo.url.String()}, PullInterval: "3s"})
+ p := safe.NewPool(context.Background())
+ defer p.Stop()
+
+ ch := make(chan dynamic.ConfigEvent)
+ defer close(ch)
+
+ err := g.Start(ch, p)
+ require.NoError(t, err)
+
+ // wait init
+ _ = wait(ch, 3*time.Second)
+
+ repo.commit(t, "foo.txt", "bar")
+
+ // git deletes the file first
+ e := wait(ch, 5*time.Second)
+ require.Equal(t, dynamic.Delete, e.Event)
+ e = wait(ch, 5*time.Second)
+ require.NotNil(t, e.Config)
+ require.Equal(t, []byte("bar"), e.Config.Raw)
+}
+
// go-git requires git installed for file:// repositories
func testGitSimpleUrl(t *testing.T) {
repo := newGitRepo(t, t.Name())
@@ -330,3 +358,17 @@ func (g *gitTestRepo) commit(t *testing.T, file, content string) {
_, err = w.Commit("added "+file, &git.CommitOptions{Author: &object.Signature{When: ts}})
require.NoError(t, err)
}
+
+func wait(ch chan dynamic.ConfigEvent, timeout time.Duration) *dynamic.ConfigEvent {
+ chTimeout := time.After(timeout)
+Stop:
+ for {
+ select {
+ case <-chTimeout:
+ break Stop
+ case e := <-ch:
+ return &e
+ }
+ }
+ return nil
+}
diff --git a/docs/guides/get-started/welcome.md b/docs/guides/get-started/welcome.md
index db62c3d76..6a187bdcb 100644
--- a/docs/guides/get-started/welcome.md
+++ b/docs/guides/get-started/welcome.md
@@ -27,6 +27,8 @@ cards:
Welcome to Mokapi! Mokapi is a powerful, flexible platform for building, testing, and monitoring API-driven applications. This guide will help you quickly understand how to get started with Mokapi and introduce you to its key features.
+> *Mokapi is your always-on API contract guardian — lightweight, transparent, and spec-driven.*
+
## Build Better Software with Mokapi
Modern applications rely on dozens of APIs—many of them outside your control. That creates friction: slow testing, fragile pipelines, and blocked teams.
@@ -56,6 +58,14 @@ Mokapi offers several key benefits:
-
Rapid API Prototyping: Use Mokapi for fast prototyping of new APIs and services. Mock APIs before they’re built or deployed to ensure that your development teams can begin their work immediately, even in the absence of a fully functional backend.
-
Collaborative API Development: Mokapi enables teams to collaborate efficiently by providing an easily accessible testing environment. Developers, QA testers, and product teams can work together on mocking and validating APIs in real time.
+## Data Privacy and Security
+
+Mokapi is designed as a local, offline-first tool. Your data stays on your machine and under your control at all times.
+
+Mokapi does not require you to create an account or log in. There is no user authentication, tracking, or identity management involved.
+
+Mokapi does not connect to any cloud services and does not sync your data anywhere. API specifications, mock configurations, requests, responses, and generated data remain local unless you explicitly choose to share them yourself.
+
## Explore how you can mock your APIs with Mokapi
Whether you are mocking APIs for local testing or validating event-driven systems, Mokapi makes the process seamless and efficient. With Mokapi’s powerful tools and integrations, you can accelerate your development cycle, reduce errors, and improve the quality of your APIs.
diff --git a/pkg/cli/command.go b/pkg/cli/command.go
index 1f39c60d0..168c28106 100644
--- a/pkg/cli/command.go
+++ b/pkg/cli/command.go
@@ -104,10 +104,10 @@ func (c *Command) Flags() *FlagSet {
},
}
- c.Flags().Bool("help", false, FlagDoc{Short: "Show help information and exit"})
+ c.Flags().BoolShort("help", "h", false, FlagDoc{Short: "Show help information and exit"})
if c.Version != "" {
- c.Flags().Bool("version", false, FlagDoc{Short: "Show version information and exit"})
+ c.Flags().BoolShort("version", "v", false, FlagDoc{Short: "Show version information and exit"})
}
}
return c.flags
diff --git a/webui/src/assets/vars.css b/webui/src/assets/vars.css
index 5af3b12ad..cc39d75f6 100644
--- a/webui/src/assets/vars.css
+++ b/webui/src/assets/vars.css
@@ -106,6 +106,9 @@
--code-tab-border-color: #3c424b;
--code-control-color-active: #eabaabff;
+ --blockquote-border-color: #eabaabff;
+ --blockquote-background-color: var(--color-background-soft);
+
--footer-background: #282b33;
}
@@ -188,5 +191,8 @@
--code-tab-border-color: #3c424b;
--code-control-color-active: rgb(8, 109, 215);
+ --blockquote-border-color: rgb(8, 109, 215);
+ --blockquote-background-color: #f8f9fa;
+
--footer-background: rgb(244 244 246);
}
\ No newline at end of file
diff --git a/webui/src/views/DocsView.vue b/webui/src/views/DocsView.vue
index d9a17953f..b79e287bb 100644
--- a/webui/src/views/DocsView.vue
+++ b/webui/src/views/DocsView.vue
@@ -430,9 +430,9 @@ blockquote {
margin-top: 2rem;
max-width: 700px;
padding: 1.5em 2em 1.5em 2em;
- border-left: 4px solid #eabaabff;
+ border-left: 4px solid var(--blockquote-border-color);
position: relative;
- background: var(--color-background-soft);
+ background-color: var(--blockquote-background-color);
}
blockquote span:before {
content: '- '
From 2eac26171ee43583a9ec25d6d92ee9cc7c7ff957 Mon Sep 17 00:00:00 2001
From: maesi
Date: Thu, 5 Feb 2026 18:48:03 +0100
Subject: [PATCH 02/75] fix help flag with shorthand flags
---
pkg/cli/docs_test.go | 18 +++++++++---------
pkg/cli/help.go | 6 ++++++
pkg/cli/help_test.go | 4 ++--
3 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/pkg/cli/docs_test.go b/pkg/cli/docs_test.go
index b5c1a04c1..db63b027b 100644
--- a/pkg/cli/docs_test.go
+++ b/pkg/cli/docs_test.go
@@ -53,9 +53,9 @@ description: A complete list of all Foo flags, with descriptions, defaults, and
Show help information and exit
-| Flag | Env | Type | Default |
-|------|------|:----:|:-------:|
-| --help | HELP | bool | false |
+| Flag | Shorthand | Env | Type | Default |
+|------|:---------:|------|:----:|:-------:|
+| --help | -h | HELP | bool | false |
`, sb.String())
},
@@ -103,9 +103,9 @@ description: A complete list of all Foo flags, with descriptions, defaults, and
Show help information and exit
-| Flag | Env | Type | Default |
-|------|------|:----:|:-------:|
-| --help | HELP | bool | false |
+| Flag | Shorthand | Env | Type | Default |
+|------|:---------:|------|:----:|:-------:|
+| --help | -h | HELP | bool | false |
`, sb.String())
},
@@ -164,9 +164,9 @@ Some long description here
Show help information and exit
-| Flag | Env | Type | Default |
-|------|------|:----:|:-------:|
-| --help | HELP | bool | false |
+| Flag | Shorthand | Env | Type | Default |
+|------|:---------:|------|:----:|:-------:|
+| --help | -h | HELP | bool | false |
`, "```bash tab=CLI\n--foo bar\n```\n```bash tab=Env\nFOO=bar\n```\n```bash tab=File\nfoo: bar\n```"), sb.String())
},
diff --git a/pkg/cli/help.go b/pkg/cli/help.go
index 9dcbedc4a..441e03f34 100644
--- a/pkg/cli/help.go
+++ b/pkg/cli/help.go
@@ -26,7 +26,13 @@ func (c *Command) printHelp() {
_, _ = fmt.Fprintf(w, "\nFlags:")
maxNameLen, hasShort := flagsInfo(flags)
+ done := map[string]bool{}
_ = flags.Visit(func(flag *Flag) error {
+ // skip aliases and short
+ if done[flag.Name] {
+ return nil
+ }
+ done[flag.Name] = true
_, _ = fmt.Fprintln(w)
if hasShort {
diff --git a/pkg/cli/help_test.go b/pkg/cli/help_test.go
index 0d7321250..8c101d11c 100644
--- a/pkg/cli/help_test.go
+++ b/pkg/cli/help_test.go
@@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
return c
}(),
test: func(t *testing.T, out string) {
- require.Equal(t, "\nFlags:\n --help Show help information and exit\n", out)
+ require.Equal(t, "\nFlags:\n -h, --help Show help information and exit\n", out)
},
},
{
@@ -31,7 +31,7 @@ func TestHelp(t *testing.T) {
return c
}(),
test: func(t *testing.T, out string) {
- require.Equal(t, "\n\nLong Description\n\nFlags:\n --help Show help information and exit\n", out)
+ require.Equal(t, "\n\nLong Description\n\nFlags:\n -h, --help Show help information and exit\n", out)
},
},
}
From 1f85e01a07a0cb25d9835a7cbcfdaa1f4f9bf28f Mon Sep 17 00:00:00 2001
From: maesi
Date: Thu, 5 Feb 2026 19:17:55 +0100
Subject: [PATCH 03/75] fix: add GIT to image for testing GIT provider
---
images/alpha.Dockerfile | 3 +++
1 file changed, 3 insertions(+)
diff --git a/images/alpha.Dockerfile b/images/alpha.Dockerfile
index 73ffde179..b2ce6bccd 100644
--- a/images/alpha.Dockerfile
+++ b/images/alpha.Dockerfile
@@ -17,6 +17,9 @@ ARG VERSION=dev
ARG BUILD_TIME=dev
+# Install git for GIT tests
+RUN apk add --no-cache git
+
COPY . /go/src/github.com/mokapi
WORKDIR /go/src/github.com/mokapi
From acb03fe0391016bf0e9eb8ece92d4c17780c6c46 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 07:43:52 +0000
Subject: [PATCH 04/75] Bump github.com/evanw/esbuild from 0.27.2 to 0.27.3
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.2 to 0.27.3.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.2...v0.27.3)
---
updated-dependencies:
- dependency-name: github.com/evanw/esbuild
dependency-version: 0.27.3
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
go.mod | 4 ++--
go.sum | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/go.mod b/go.mod
index e57cad3d8..a75ab4d4d 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,11 @@ require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c
- github.com/evanw/esbuild v0.27.2
+ github.com/evanw/esbuild v0.27.3
github.com/fsnotify/fsnotify v1.9.0
github.com/go-co-op/gocron v1.37.0
github.com/go-git/go-git/v5 v5.16.4
+ github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0
github.com/pkg/errors v0.9.1
@@ -58,7 +59,6 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
- github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-github/v75 v75.0.0 // indirect
diff --git a/go.sum b/go.sum
index 7ecb3f6ff..1d553f2a9 100644
--- a/go.sum
+++ b/go.sum
@@ -81,8 +81,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg=
-github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
+github.com/evanw/esbuild v0.27.3 h1:dH/to9tBKybig6hl25hg4SKIWP7U8COdJKbGEwnUkmU=
+github.com/evanw/esbuild v0.27.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
From 0dadf11ce8f3e73216b0516a4ee534bdd94f217e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 07:43:54 +0000
Subject: [PATCH 05/75] Bump vue-router from 5.0.0 to 5.0.2 in /webui
Bumps [vue-router](https://github.com/vuejs/router) from 5.0.0 to 5.0.2.
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v5.0.0...v5.0.2)
---
updated-dependencies:
- dependency-name: vue-router
dependency-version: 5.0.2
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 21 ++++++++++-----------
webui/package.json | 2 +-
2 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index e0d2b55ee..a7b35f45c 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -30,7 +30,7 @@
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
"vue": "^3.5.27",
- "vue-router": "^5.0.0",
+ "vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
"vue3-markdown-it": "^1.0.10",
@@ -7147,18 +7147,17 @@
}
},
"node_modules/unplugin": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
- "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
+ "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
- "acorn": "^8.15.0",
"picomatch": "^4.0.3",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
- "node": ">=18.12.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/unplugin-utils": {
@@ -7425,9 +7424,9 @@
}
},
"node_modules/vue-router": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.0.tgz",
- "integrity": "sha512-xWHlps4o1ScODWqvyapl0v1uGy0g7ozmsTSO/dguyGb/9RL6oSU2HfN/8oMXnoFOH1BuTaAkbiOz4OWdkfjcZg==",
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz",
+ "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.28.6",
@@ -7444,7 +7443,7 @@
"picomatch": "^4.0.3",
"scule": "^1.3.0",
"tinyglobby": "^0.2.15",
- "unplugin": "^2.3.11",
+ "unplugin": "^3.0.0",
"unplugin-utils": "^0.3.1",
"yaml": "^2.8.2"
},
@@ -7452,7 +7451,7 @@
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
- "@pinia/colada": "^0.18.1",
+ "@pinia/colada": ">=0.21.2",
"@vue/compiler-sfc": "^3.5.17",
"pinia": "^3.0.4",
"vue": "^3.5.0"
diff --git a/webui/package.json b/webui/package.json
index 9b188f55c..a39481e7d 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -39,7 +39,7 @@
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
"vue": "^3.5.27",
- "vue-router": "^5.0.0",
+ "vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
"vue3-markdown-it": "^1.0.10",
From b059bf8052850474de717610a9e9174ea2b19423 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 07:51:41 +0000
Subject: [PATCH 06/75] Bump @vitejs/plugin-vue from 6.0.3 to 6.0.4 in /webui
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.4/packages/plugin-vue)
---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
dependency-version: 6.0.4
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 16 ++++++++--------
webui/package.json | 2 +-
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index a7b35f45c..1564eb416 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -42,7 +42,7 @@
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.1.0",
- "@vitejs/plugin-vue": "^6.0.3",
+ "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
@@ -911,9 +911,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-beta.53",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
- "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
"dev": true,
"license": "MIT"
},
@@ -1632,13 +1632,13 @@
}
},
"node_modules/@vitejs/plugin-vue": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
- "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==",
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz",
+ "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@rolldown/pluginutils": "1.0.0-beta.53"
+ "@rolldown/pluginutils": "1.0.0-rc.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
diff --git a/webui/package.json b/webui/package.json
index a39481e7d..b16e2a69c 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -51,7 +51,7 @@
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.1.0",
- "@vitejs/plugin-vue": "^6.0.3",
+ "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
From 8bcc58ab45cbc432eee01dae57b248f62bd71211 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 07:59:16 +0000
Subject: [PATCH 07/75] Bump @types/node from 25.1.0 to 25.2.1 in /webui
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.1.0 to 25.2.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
---
updated-dependencies:
- dependency-name: "@types/node"
dependency-version: 25.2.1
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 8 ++++----
webui/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 1564eb416..6a312db3b 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -41,7 +41,7 @@
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
- "@types/node": "^25.1.0",
+ "@types/node": "^25.2.1",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
@@ -1365,9 +1365,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz",
- "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==",
+ "version": "25.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
+ "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
diff --git a/webui/package.json b/webui/package.json
index b16e2a69c..d77535164 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -50,7 +50,7 @@
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
- "@types/node": "^25.1.0",
+ "@types/node": "^25.2.1",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
From e149f3e69882e83059f3fa9359adad70e930c8f6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 08:07:36 +0000
Subject: [PATCH 08/75] Bump ldapts from 8.1.3 to 8.1.6 in /webui
Bumps [ldapts](https://github.com/ldapts/ldapts) from 8.1.3 to 8.1.6.
- [Release notes](https://github.com/ldapts/ldapts/releases)
- [Changelog](https://github.com/ldapts/ldapts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ldapts/ldapts/compare/v8.1.3...v8.1.6)
---
updated-dependencies:
- dependency-name: ldapts
dependency-version: 8.1.6
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 46 ++++++-----------------------------------
webui/package.json | 2 +-
2 files changed, 7 insertions(+), 41 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 6a312db3b..6168f93ec 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -25,7 +25,7 @@
"http-status-codes": "^2.3.0",
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
- "ldapts": "^8.1.3",
+ "ldapts": "^8.1.6",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
@@ -4817,13 +4817,12 @@
}
},
"node_modules/ldapts": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.3.tgz",
- "integrity": "sha512-kEU3GDh48ZymnyLGsFprai2v4r7Gyxe6niBlUUw3xnOGpq5O+XODmXJ8gBwbPIg35qt5cnYVC80NNSdAkb2dJg==",
+ "version": "8.1.6",
+ "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.6.tgz",
+ "integrity": "sha512-sofxzGEPRBvubSrdmly0mmUwjXHPfTbO51KLAUzuO4sHWwy+r0G6FwaLWWDwTPRpjJFkMdLId5BeRUHksUH4yA==",
"license": "MIT",
"dependencies": {
- "strict-event-emitter-types": "2.0.0",
- "whatwg-url": "15.1.0"
+ "strict-event-emitter-types": "2.0.0"
},
"engines": {
"node": ">=20"
@@ -5976,6 +5975,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -6920,18 +6920,6 @@
"node": ">=0.6"
}
},
- "node_modules/tr46": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
- "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
- "license": "MIT",
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -7543,15 +7531,6 @@
"markdown-it-toc-done-right": "^4.2.0"
}
},
- "node_modules/webidl-conversions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
- "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@@ -7567,19 +7546,6 @@
"node": ">=20"
}
},
- "node_modules/whatwg-url": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
- "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
- "license": "MIT",
- "dependencies": {
- "tr46": "^6.0.0",
- "webidl-conversions": "^8.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/webui/package.json b/webui/package.json
index d77535164..58606c63c 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -34,7 +34,7 @@
"http-status-codes": "^2.3.0",
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
- "ldapts": "^8.1.3",
+ "ldapts": "^8.1.6",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
From b527c58e408e2f4a8e051fa51341e9ab5bf362ba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Feb 2026 08:15:46 +0000
Subject: [PATCH 09/75] Bump nodemailer from 7.0.13 to 8.0.0 in /webui
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.13 to 8.0.0.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0)
---
updated-dependencies:
- dependency-name: nodemailer
dependency-version: 8.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 8 ++++----
webui/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 6168f93ec..d3414d105 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -28,7 +28,7 @@
"ldapts": "^8.1.6",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
- "nodemailer": "^7.0.13",
+ "nodemailer": "^8.0.0",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
@@ -5269,9 +5269,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
- "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.0.tgz",
+ "integrity": "sha512-xvVJf/f0bzmNpnRIbhCp/IKxaHgJ6QynvUbLXzzMRPG3LDQr5oXkYuw4uDFyFYs8cge8agwwrJAXZsd4hhMquw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
diff --git a/webui/package.json b/webui/package.json
index 58606c63c..68b617677 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -37,7 +37,7 @@
"ldapts": "^8.1.6",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
- "nodemailer": "^7.0.13",
+ "nodemailer": "^8.0.0",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
From da8f20c32d90e146bf55cc304fc6cb7eb6572b10 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 6 Feb 2026 23:31:58 +0100
Subject: [PATCH 10/75] update documentation Welcome and Installation
---
docs/guides/get-started/installation.md | 69 ++++++++++++++++++-------
docs/guides/get-started/welcome.md | 58 ++++++++++-----------
webui/package-lock.json | 25 +++++++--
webui/package.json | 2 +
webui/src/assets/tabs.css | 42 ++++++++++++++-
webui/src/assets/vars.css | 8 ++-
webui/src/composables/markdown.ts | 4 +-
7 files changed, 148 insertions(+), 60 deletions(-)
diff --git a/docs/guides/get-started/installation.md b/docs/guides/get-started/installation.md
index 8bd6b15d4..7b6311620 100644
--- a/docs/guides/get-started/installation.md
+++ b/docs/guides/get-started/installation.md
@@ -4,18 +4,46 @@ description: Learn how to install Mokapi effortlessly across Windows, macOS, and
---
# Install Mokapi
-Mokapi is an open-source tool designed to simplify API mocking and schema validation, enabling developers to prototype, test, and demonstrate APIs with realistic data and scenarios. This guide provides step-by-step instructions to install Mokapi on various platforms, ensuring a smooth setup process.
+Mokapi is an open-source tool designed to simplify API mocking and schema validation.
+It enables developers to prototype, test, and demonstrate APIs with realistic data and
+scenarios. This guide provides straightforward instructions to install Mokapi on various
+platforms.
-## Docker
+## Installation Options
-Visit [DockerHub](https://hub.docker.com/r/mokapi/mokapi/tags) for a list of all available images.
-You can also use a custom base Docker image as shown in [these examples](/docs/resources/examples/mokapi-with-custom-base-image.md).
+Mokapi can be installed via direct download or through package managers on supported platforms.
+Choose your preferred method below:
+::: tabs
+
+@tab "macOS"
+
+### Homebrew
+
+```bash
+brew tap marle3003/tap
+brew install mokapi
```
-docker pull mokapi/mokapi
+
+### Direct Download
+
+Download the latest macOS version from [GitHub](https://github.com/marle3003/mokapi/releases)
+
+@tab "Windows"
+
+### Chocolatey
+
+```Powershell
+choco install mokapi
```
-## Linux
+### Direct Download
+
+Download the latest Windows version from [GitHub](https://github.com/marle3003/mokapi/releases)
+
+@tab "Linux"
+
+### Direct Download
Download file appropriate for your Linux distribution and ARCH from the [release page](https://github.com/marle3003/mokapi/releases), then install with
@@ -27,32 +55,33 @@ dpkg -i mokapi_{version}_linux_{arch}.deb
rpm -i mokapi_{version}_linux_{arch}.rpm
```
-## MacOS
+@tab "Docker"
-Using [Homebrew](https://brew.sh/):
+To get started with Mokapi using Docker, visit [DockerHub](https://hub.docker.com/r/mokapi/mokapi/tags) for a list of available images.
+You can also use a custom base Docker image as demonstrated in [these examples](/docs/resources/examples/mokapi-with-custom-base-image.md).
```
-brew tap marle3003/tap
-brew install mokapi
+docker pull mokapi/mokapi
```
-## Windows
-
-Install Mokapi by [Chocolatey package manager](https://chocolatey.org/) with:
-
-```Powershell
-choco install mokapi
-```
+@tab "NPM"
-## NodeJS npm package
+If you prefer to install Mokapi as a Node.js package, use the following command:
```bash
npm install go-mokapi
```
-## Download binary
+:::
+
+### Mokapi Scripts Type Definitions
-You can download Mokapi's binary file from [GitHub Releases page](https://github.com/marle3003/mokapi/releases)
+Mokapi allows you to write **custom scripts** to handle API events or modify responses.
+For full type safety and autocompletion in TypeScript, you can install the [`@types/mokapi`](https://www.npmjs.com/package/@types/mokapi`) package:
+
+```bash
+npm install --save-dev @types/mokapi
+```
## Next steps
diff --git a/docs/guides/get-started/welcome.md b/docs/guides/get-started/welcome.md
index 6a187bdcb..cec0d0f02 100644
--- a/docs/guides/get-started/welcome.md
+++ b/docs/guides/get-started/welcome.md
@@ -25,49 +25,47 @@ cards:
# Mocking APIs with Mokapi
-Welcome to Mokapi! Mokapi is a powerful, flexible platform for building, testing, and monitoring API-driven applications. This guide will help you quickly understand how to get started with Mokapi and introduce you to its key features.
+**Welcome to Mokapi!**
-> *Mokapi is your always-on API contract guardian — lightweight, transparent, and spec-driven.*
+Mokapi is your go-to platform for mocking APIs, making it easy to build,
+test, and monitor API-driven applications without the hassle.
-## Build Better Software with Mokapi
-
-Modern applications rely on dozens of APIs—many of them outside your control. That creates friction: slow testing, fragile pipelines, and blocked teams.
-
-Mokapi was created to remove those barriers.
-It empowers developers to mock, simulate, and explore APIs with ease, so you can:
+> *Think of Mokapi as your ever-reliable API contract guardian—lightweight, transparent, and specification-driven.*
-- **Develop without waiting** for external systems to be ready.
-- **Test with confidence** against realistic API behavior.
-- **Automate your workflows with CI/CD pipelines that stay reliable.**
-- **Stay future-proof** by safely integrating with tools like Dependabot or Renovate.
-
-Mokapi is more than just a mocking tool—it’s a way to unlock faster feedback, smoother collaboration, and higher quality software.
+## Build Better Software with Mokapi
-> Because building and testing great software shouldn’t depend on systems you can’t control.
+In today's world, modern applications rely on multiple external APIs.
+When these APIs are slow, unreliable, or unavailable, they can impede
+your development process. Mokapi eliminates these obstacles, enabling
+you to:
-## Why Choose Mokapi?
+- **Develop Faster:** Eliminate waiting times by working independently of external systems.
+- **Test with Confidence:** Simulate realistic API behaviors, including edge cases.
+- **Automate Your Pipelines:** Enhance CI/CD reliability with consistent mock responses.
+- **Ensure Compliance:** Seamlessly integrate tools like Dependabot or Renovate for future-proof dependencies.
-Mokapi offers several key benefits:
+## Key Features
--
Easy API Mocking: Quickly mock REST, SOAP, or event-driven APIs with minimal setup. Mokapi automatically generates mock servers based on your OpenAPI or AsyncAPI specifications, allowing you to simulate and test APIs in seconds.
--
No-Code Setup: Mokapi allows you to mock and test APIs with no coding effort.
--
Real-Time Monitoring and Analytics: With Mokapi’s interactive dashboard, you can monitor and analyze requests and responses in real-time. Get detailed insights into API behavior, errors, and status codes to help streamline debugging and testing.
--
Flexible Test Data Generation: Mokapi’s random data generator allows you to simulate realistic test data. You can customize data patterns and even create dynamic data based on your APIs needs, making it ideal for testing edge cases and complex workflows.
--
Seamless CI/CD Integration: Mokapi easily integrates into your continuous integration and deployment (CI/CD) pipeline. Run automated tests and validations for your APIs without any manual intervention, improving efficiency and reducing human error.
--
Free and Open-Source: Mokapi is an open-source project, so you can start using it without any licensing fees. It’s free to use and offers transparency, flexibility, and the ability to contribute to the project’s development.
--
Rapid API Prototyping: Use Mokapi for fast prototyping of new APIs and services. Mock APIs before they’re built or deployed to ensure that your development teams can begin their work immediately, even in the absence of a fully functional backend.
--
Collaborative API Development: Mokapi enables teams to collaborate efficiently by providing an easily accessible testing environment. Developers, QA testers, and product teams can work together on mocking and validating APIs in real time.
+-
Spec-Driven Mocking: Quickly create OpenAPI or AsyncAPI mock servers for REST, SOAP, and event-driven architectures with minimal setup.
+-
No-Code Configuration: Jump right in—no complex coding required!
+-
Live Monitoring: Use the [Mokapi Dashboard](docs/guides/get-started/dashboard) to track requests and responses in real-time, simplifying your debugging process.
+-
Dynamic Test Data: Utilize the built-in random data generator to create realistic payloads tailored to your API needs.
+-
Local & Secure: An offline-first tool, Mokapi keeps your data on your machine—no accounts, no cloud syncs, and no identity tracking.
+-
Open Source: Free and transparent, explore Mokapi's source code on [GitHub](https://github.com/marle3003/mokapi).
## Data Privacy and Security
-Mokapi is designed as a local, offline-first tool. Your data stays on your machine and under your control at all times.
-
-Mokapi does not require you to create an account or log in. There is no user authentication, tracking, or identity management involved.
+Mokapi prioritizes your privacy:
-Mokapi does not connect to any cloud services and does not sync your data anywhere. API specifications, mock configurations, requests, responses, and generated data remain local unless you explicitly choose to share them yourself.
+- **Local Control:** Your data remains on your machine at all times.
+- **No Account Needed:** Enjoy a hassle-free experience without the need for user accounts or authentication.
+- **No Cloud Connectivity:** Mokapi operates entirely offline, ensuring your data—including API specifications, mock configurations, requests, and responses—stays private.
## Explore how you can mock your APIs with Mokapi
-Whether you are mocking APIs for local testing or validating event-driven systems, Mokapi makes the process seamless and efficient. With Mokapi’s powerful tools and integrations, you can accelerate your development cycle, reduce errors, and improve the quality of your APIs.
+Whether you're mocking APIs for local testing or validating event-driven
+systems, Mokapi streamlines the process. With its powerful tools and
+integrations, you can accelerate your development cycle, minimize errors,
+and enhance the quality of your APIs.
{{ card-grid key="cards" }}
diff --git a/webui/package-lock.json b/webui/package-lock.json
index e0d2b55ee..1f0c9a31e 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -26,6 +26,7 @@
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
"ldapts": "^8.1.3",
+ "markdown-it-container": "^4.0.0",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
@@ -41,6 +42,7 @@
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
+ "@types/markdown-it-container": "^4.0.0",
"@types/node": "^25.1.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -1337,26 +1339,33 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
+ "node_modules/@types/markdown-it-container": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it-container/-/markdown-it-container-4.0.0.tgz",
+ "integrity": "sha512-GmD8OECLfzPHv8VyvFRzslqdwXoDBJ2H40fxXFjrarbqvJZSB/BJKZXN5e3k7Mx7GQanSNzTYhzeS3H9o0gAOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": ">=14"
+ }
+ },
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/mokapi": {
"version": "0.29.1",
@@ -4970,6 +4979,12 @@
"markdown-it": "*"
}
},
+ "node_modules/markdown-it-container": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-container/-/markdown-it-container-4.0.0.tgz",
+ "integrity": "sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==",
+ "license": "MIT"
+ },
"node_modules/markdown-it-deflist": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
diff --git a/webui/package.json b/webui/package.json
index 9b188f55c..82046f2c4 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -35,6 +35,7 @@
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
"ldapts": "^8.1.3",
+ "markdown-it-container": "^4.0.0",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^7.0.13",
@@ -50,6 +51,7 @@
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
+ "@types/markdown-it-container": "^4.0.0",
"@types/node": "^25.1.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/eslint-config-prettier": "^10.2.0",
diff --git a/webui/src/assets/tabs.css b/webui/src/assets/tabs.css
index 0cdf5b51e..e2a914c38 100644
--- a/webui/src/assets/tabs.css
+++ b/webui/src/assets/tabs.css
@@ -69,7 +69,45 @@
.tab-pane {
padding: 0.8rem;
padding-top: 1.6rem;
- }
+}
+
+.tabs {
+ border: none;
+ padding: 16px 0;
+
+ .nav-tabs {
+ border: none;
+ }
+
+ > .nav-tabs button {
+ border: none;
+ background-color: transparent;
+ padding: 0 8px 4px;
+ }
+
+ > .nav-tabs button[role="tab"].active, > .nav-tabs button[role="tab"]:hover {
+ border-color: var(--code-tabs-border-color-active);
+ border-bottom-width: 3px;
+ border-bottom-style: solid;
+ margin-bottom: -3px;
+ }
+
+ > .nav-tabs .tabs-border {
+ width: 100%;
+ height: 2px;
+ background-color: var(--tabs-border-color);
+ margin: 0 auto;
+ }
+
+ .tab-content {
+ margin-top: 20px;
+ }
+
+ .tab-pane {
+ padding: 0;
+ }
+}
+
.code {
background-color: var(--code-background);
color: var(--code-color);
@@ -123,6 +161,6 @@
.tabs-border {
width: 100%;
height: 3px;
- background-color: var(--code-tab-border-color);
+ background-color: var(--code-tabs-border-color);
margin: 0 auto;
}
\ No newline at end of file
diff --git a/webui/src/assets/vars.css b/webui/src/assets/vars.css
index cc39d75f6..55b16509f 100644
--- a/webui/src/assets/vars.css
+++ b/webui/src/assets/vars.css
@@ -98,12 +98,14 @@
--badge-background: #eabaabff;
+ --tabs-border-color: rgba(255, 255, 255, 0.1);
+
--code-background: #0d1117;
--code-color: #fff;
--code-tabs-color: #d3d4d5;
--code-tabs-color-active: #eabaabff;
--code-tabs-border-color-active: #eabaabff;
- --code-tab-border-color: #3c424b;
+ --code-tabs-border-color: #3c424b;
--code-control-color-active: #eabaabff;
--blockquote-border-color: #eabaabff;
@@ -183,12 +185,14 @@
--badge-background: rgb(8, 109, 215);
+ --tabs-border-color: rgba(0, 0, 0, 0.1);
+
--code-background: #0d1117;
--code-color: rgb(255, 255, 255);;
--code-tabs-color: rgb(255,255,255);
--code-tabs-color-active: rgb(255, 255, 255);
--code-tabs-border-color-active: rgb(8, 109, 215);
- --code-tab-border-color: #3c424b;
+ --code-tabs-border-color: #3c424b;
--code-control-color-active: rgb(8, 109, 215);
--blockquote-border-color: rgb(8, 109, 215);
diff --git a/webui/src/composables/markdown.ts b/webui/src/composables/markdown.ts
index 71e5b49e8..e5fb3265b 100644
--- a/webui/src/composables/markdown.ts
+++ b/webui/src/composables/markdown.ts
@@ -7,6 +7,7 @@ import { MarkdownItCard } from '@/composables/markdown-card';
import { MarkdownItCarousel } from './markdown-carousel';
import yaml from 'js-yaml'
import { MarkdownItBlockquote } from './markdown-blockquote';
+import { MarkdownItTabContent } from './markdown-tab-content';
const images = import.meta.glob('/src/assets/docs/**/*.png', {as: 'url', eager: true})
const metadataRegex = /^---([\s\S]*?)---/;
@@ -26,6 +27,7 @@ export function useMarkdown(content: string | undefined): {content: string | und
.use(MarkdownItHighlightjs)
.use(MarkdownItBlockquote)
.use(MarkdownItTabs)
+ .use(MarkdownItTabContent)
.use(MarkdownItBox)
.use(MarkdownItLinks)
.use(MarkdownItCarousel(metadata))
@@ -36,7 +38,7 @@ export function useMarkdown(content: string | undefined): {content: string | und
return {content, metadata}
} catch (e) {
- console.error('invalid markdown: '+content)
+ console.error('invalid markdown: '+e)
return { content: '' }
}
}
From 67010fa4ef3eb11943f54ddb92290f575be98419 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 6 Feb 2026 23:56:07 +0100
Subject: [PATCH 11/75] update package-lock.json
---
webui/package-lock.json | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index d3414d105..4cd9b0e02 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -26,6 +26,7 @@
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
"ldapts": "^8.1.6",
+ "markdown-it-container": "^4.0.0",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^8.0.0",
@@ -41,6 +42,7 @@
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
+ "@types/markdown-it-container": "^4.0.0",
"@types/node": "^25.2.1",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -1337,26 +1339,33 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
+ "node_modules/@types/markdown-it-container": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it-container/-/markdown-it-container-4.0.0.tgz",
+ "integrity": "sha512-GmD8OECLfzPHv8VyvFRzslqdwXoDBJ2H40fxXFjrarbqvJZSB/BJKZXN5e3k7Mx7GQanSNzTYhzeS3H9o0gAOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": ">=14"
+ }
+ },
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/mokapi": {
"version": "0.29.1",
@@ -4969,6 +4978,12 @@
"markdown-it": "*"
}
},
+ "node_modules/markdown-it-container": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-container/-/markdown-it-container-4.0.0.tgz",
+ "integrity": "sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==",
+ "license": "MIT"
+ },
"node_modules/markdown-it-deflist": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
From 90e81f3d0187fcc1d12ec321a98ede914d7ea5d6 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 6 Feb 2026 23:58:49 +0100
Subject: [PATCH 12/75] add missing file
---
webui/src/composables/markdown-tab-content.ts | 134 ++++++++++++++++++
1 file changed, 134 insertions(+)
create mode 100644 webui/src/composables/markdown-tab-content.ts
diff --git a/webui/src/composables/markdown-tab-content.ts b/webui/src/composables/markdown-tab-content.ts
new file mode 100644
index 000000000..c273c39e0
--- /dev/null
+++ b/webui/src/composables/markdown-tab-content.ts
@@ -0,0 +1,134 @@
+import type MarkdownIt from "markdown-it"
+import type { Options } from "markdown-it"
+import container from "markdown-it-container"
+import type { Renderer } from "markdown-it/dist/index.cjs.js"
+import Token from 'markdown-it/lib/token'
+
+export function MarkdownItTabContent(md: MarkdownIt, _opts: Options) {
+
+ md.core.ruler.push('test', function (state) {
+ let hide = false
+ let inTab = false
+
+ for (let i = 0; i < state.tokens.length; i++) {
+ const token = state.tokens[i]!
+
+ switch (token.type) {
+ case 'container_tabs_open':
+ hide = true
+ continue
+ case 'container_tabs_close':
+ hide = false
+ inTab = false
+ continue
+ case 'inline':
+ if (token.content.startsWith('@tab')) {
+ const match = token.content.trim().match(/^@tab "(.*)"$/);
+ if (!match || !match[1]) {
+ console.error('invalid tab format: ' + token.content)
+ return
+ }
+ const label = match[1];
+ const safe = label.replace(/[^\w-]/g, "-");
+ const tabIndex = tabs.length
+ const tabId = `tab-${tabIndex}-${safe}`
+ const panelId = `tabPanel-${tabIndex}-${safe}`
+
+ const button = `
+ `
+
+ tabs.push({
+ index: tabIndex,
+ panelId: panelId,
+ button: button,
+ panel: '',
+ tokens: []
+ })
+ inTab = true
+ hideToken(token);
+ i++; // skip closing paragraph
+ continue
+ }
+ }
+
+ if (hide) {
+ hideToken(token)
+ if (inTab) {
+ tabs[tabs.length - 1]!.tokens.push(token)
+ }
+ }
+ }
+ })
+
+ function hideToken(token: Token) {
+ token.hidden = true
+ if (token.children) {
+ for (const child of token.children) {
+ hideToken(child)
+ }
+ }
+ }
+
+ interface TabsState {
+ index: number
+ panelId: string
+ button: string
+ panel: string
+ tokens: Token[]
+ }
+
+ const tabs: TabsState[] = []
+
+ md.use(container, 'tabs', {
+ validate: (params: any) => params.trim() === 'tabs',
+ render: (tokens: Token[], idx: number, options: Options, env: any, slf: Renderer) => {
+ const token = tokens[idx];
+ if (token?.nesting === 1) {
+ return ''
+ } else {
+ return `
+
Ensure consistent testing across local development, CI pipelines, and cloud environments.
-
+
Learn How to Run Mokapi
@@ -371,7 +371,7 @@ function showImage(evt: MouseEvent) {
Produce lifelike data to catch bugs early and test edge cases that rarely occur in production.
-
+
Explore Fake Data Features
@@ -416,7 +416,7 @@ function showImage(evt: MouseEvent) {
Live Dashboard Demo
-
+
How it works
@@ -485,7 +485,7 @@ function showImage(evt: MouseEvent) {
Mock SMTP Email Sending
Simulate an SMTP server and send test emails using Node.js. Perfect for validating email workflows without real mail servers.
- Start Tutorial
+ Start Tutorial
@@ -501,7 +501,7 @@ function showImage(evt: MouseEvent) {
Enforce API Contracts
Validate HTTP requests and responses against OpenAPI specs to catch API issues early in development or testing.
- Read Blog
+ Read Blog
@@ -547,7 +547,7 @@ function showImage(evt: MouseEvent) {
diff --git a/webui/src/views/Http.vue b/webui/src/views/Http.vue
index 35c148a41..eeb2afa31 100644
--- a/webui/src/views/Http.vue
+++ b/webui/src/views/Http.vue
@@ -78,10 +78,10 @@ function getConsoleContent() {
without waiting for real backends.
-
+
-
+
@@ -91,10 +91,10 @@ function getConsoleContent() {
-
+
-
+
@@ -164,7 +164,7 @@ function getConsoleContent() {
Test Without Dependencies
Run faster, more reliable tests by simulating external dependencies in CI/CD pipelines.
Mokapi’s interactive dashboard provides real-time insights into every request and response. With visual tracking, detailed logs, and performance analytics, you can quickly understand what's going on with your mocks and optimize your testing process.
-
+
diff --git a/webui/src/views/Kafka.vue b/webui/src/views/Kafka.vue
index d328b0873..ef325a973 100644
--- a/webui/src/views/Kafka.vue
+++ b/webui/src/views/Kafka.vue
@@ -44,10 +44,10 @@ function showImage(evt: MouseEvent) {
-
+
-
+
@@ -57,10 +57,10 @@ function showImage(evt: MouseEvent) {
@@ -91,11 +91,11 @@ function showImage(evt: MouseEvent) {
-
-
+
+ Get Started
-
-
+
+ Tutorials
@@ -312,7 +312,7 @@ function showImage(evt: MouseEvent) {
Simulate edge cases, conditional logic, errors, and real-world workflows
without changing your API specifications.
-
+
Explore JavaScript Mocking
@@ -351,7 +351,7 @@ function showImage(evt: MouseEvent) {
Version-controlled mocks reduce errors, simplify audits, and make collaboration easier.
-
+
Learn More
@@ -446,7 +446,7 @@ function showImage(evt: MouseEvent) {
Mock REST APIs with OpenAPI
Learn how to mock an OpenAPI spec, configure Mokapi, and run it in Docker. Test REST endpoints without waiting for live APIs.
- Start Tutorial
+ Start Tutorial
@@ -459,7 +459,7 @@ function showImage(evt: MouseEvent) {
Simulate Kafka Topics with AsyncAPI
Test Kafka producers and consumers by mocking topics according to your AsyncAPI spec. Ensure reliable message generation and integration without a live Kafka cluster.
With Mokapi Scripts, you can quickly customize API responses to match your exact test conditions. Use event handlers to simulate logic, errors, or edge cases without implementing server logic.
-
+
diff --git a/webui/src/views/Kafka.vue b/webui/src/views/Kafka.vue
index ef325a973..aa9971c30 100644
--- a/webui/src/views/Kafka.vue
+++ b/webui/src/views/Kafka.vue
@@ -44,11 +44,11 @@ function showImage(evt: MouseEvent) {
-
-
+
+ Get Started
-
-
+
+ Documentation
@@ -57,11 +57,11 @@ function showImage(evt: MouseEvent) {
-
-
+
+ Get Started
-
-
+
+ Documentation
diff --git a/webui/src/views/Ldap.vue b/webui/src/views/Ldap.vue
index eb66bb793..5255db2b8 100644
--- a/webui/src/views/Ldap.vue
+++ b/webui/src/views/Ldap.vue
@@ -58,11 +58,11 @@ function showImage(evt: MouseEvent) {
-
-
+
+ Get Started
-
-
+
+ Documentation
@@ -71,11 +71,11 @@ function showImage(evt: MouseEvent) {
-
-
+
+ Get Started
-
-
+
+ Documentation
@@ -232,7 +232,7 @@ function showImage(evt: MouseEvent) {
Test login flows, password policies, and group permissions.
@@ -236,7 +245,7 @@ useMeta('Dashboard | mokapi.io', description, '')
\ No newline at end of file
From 8bdb6d19c35ad01591c1e684263f32c32482c098 Mon Sep 17 00:00:00 2001
From: maesi
Date: Thu, 12 Feb 2026 23:46:53 +0100
Subject: [PATCH 39/75] docs: update HTTP overview doc
---
docs/http/overview.md | 367 ++++++++++++++++++++++++++++++++++--------
1 file changed, 302 insertions(+), 65 deletions(-)
diff --git a/docs/http/overview.md b/docs/http/overview.md
index 749515ac9..359067198 100644
--- a/docs/http/overview.md
+++ b/docs/http/overview.md
@@ -2,53 +2,113 @@
title: How to mock HTTP APIs with Mokapi
description: Mock any HTTP API with OpenAPI specification
---
+
# Mocking HTTP APIs
-Mokapi makes it easy to mock HTTP APIs, enabling developers to test and debug their applications with minimal effort. Whether you need to validate request handling, simulate complex API responses, or troubleshoot edge cases, Mokapi provides a versatile and developer-friendly solution tailored for HTTP API testing.
+## Quick Start: Mock an API in seconds
+
+Run this single command to start mocking Swagger's PetStore API:
+
+```bash
+mokapi https://petstore3.swagger.io/api/v3/openapi.json
+```
+
+Open your browser and navigate to `http://localhost/api/v3/pet/12`. You'll see a generated response like:
+
+```json
+{
+ "id": 12,
+ "name": "Bruiser",
+ "category": {
+ "id": 1,
+ "name": "Dogs"
+ },
+ "photoUrls": ["https://example.com/photo1.jpg"],
+ "status": "available"
+}
+```
+
+That's it! Mokapi automatically generates realistic data based on your OpenAPI specification.
+
+## What You'll Learn
-Designed to integrate seamlessly with your projects, Mokapi lets you create mock APIs using the OpenAPI Specification. It generates dynamic HTTP responses based on your API definitions, eliminating the need for a live server during development. This flexibility empowers developers to experiment, prototype, and troubleshoot more effectively.
+By the end of this guide, you'll know how to:
+- Launch a mock HTTP API using an OpenAPI specification
+- Customize responses with Mokapi Scripts
+- Work with both OpenAPI 3.0 and Swagger 2.0 specifications
+- Simulate different API scenarios for testing
-With Mokapi, you can go beyond basic mocks by writing custom scripts to control the behavior of your APIs. This allows you to simulate a wide range of scenarios, such as conditional responses or stateful interactions. Mokapi also supports fetching API definitions directly from URLs or files, making it simple to get started with existing OpenAPI documents.
+## Prerequisites
-By leveraging Mokapi, you can streamline your development workflow, reduce dependencies on external systems, and deliver robust HTTP API integrations with confidence.
+Before you start, make sure you have:
-Learn how to create your first HTTP API mock with Mokapi and begin ensuring the reliability and robustness of your application.
+- Mokapi installed on your system ([installation guide](/docs/get-started/installation.md))
+- An OpenAPI 3.0 or Swagger 2.0 specification file (or URL)
+- For custom scripts, basic TypeScript or JavaScript knowledge
-## Before you start
+## Basic Usage
-You can run Mokapi in multiple ways based on your needs. Learn how to configure and launch Mokapi on your local machine by following the instructions [here](/docs/get-started/installation.md).
+### Using a Remote Specification
+Point Mokapi to any publicly accessible OpenAPI specification:
-## Launch Mokapi with Swagger's PetStore API
+```bash
+mokapi https://petstore3.swagger.io/api/v3/openapi.json
+```
+```text box=tip
+Mokapi supports both simplified syntax (`mokapi <url>`) and
+verbose flags (`mokapi --providers-http-url <url>`). This guide uses the
+simplified syntax for clarity.
+```
+**What happens:**
+1. Mokapi downloads the specification
+2. Starts the HTTP server on hosts and ports defined in `servers` specification (default port: 80)
+3. Creates HTTP endpoints for all defined paths
+4. Generates responses matching your schema definitions
+5. Starts a dashboard server on `http://localhost` (default port: 8080)
+
+The dashboard shows all available endpoints, recent requests, and response statistics.
+The API server and dashboard run independently and can use different ports.
+
+**Try it:**
+```bash
+curl http://localhost/api/v3/pet/12
+```
-To get started quickly, you can use Swagger's PetStore API specification hosted online:
+### Using a Local Specification File
-```bash tab=CLI
-mokapi --providers-http-url https://petstore3.swagger.io/api/v3/openapi.json
+```bash
+mokapi /path/to/your/openapi.yaml
```
-That’s all you need! Open your browser and navigate to http://localhost/api/v3/pet/12 to see Mokapi's generated API response based on the PetStore specification.
+This works with both `.json` and `.yaml` files.
-``` box=info
-If you encounter issues fetching the specification file from swagger.io, you might need to configure a proxy server using this command:
---providers-http-proxy http://proxy.server.com:port
+### Behind a Proxy?
+
+If you need to fetch specifications through a proxy server:
+
+```bash
+mokapi --providers-http-proxy http://proxy.server.com:8080 -- https://petstore3.swagger.io/api/v3/openapi.json
```
-## Customizing HTTP Responses with Mokapi
+## Customizing Responses with Mokapi Scripts
-For more dynamic control, Mokapi allows you to define custom HTTP responses using Mokapi Scripts. This lets you simulate various scenarios, adjust responses based on request parameters, or handle specific conditions.
+To control API behavior, you can create custom scripts that define specific responses, simulate errors, or add conditional logic.
-### Example: Custom Response for particular Pet ID
+### Example: Custom Response for a Specific Pet
-Create a petstore.ts script to define a custom response for /pet/12:
+Let's return a specific response when requesting pet ID 12.
-```typescript tab=petstore.ts (TypeScript)
+**Step 1:** Create a script file `petstore.ts`
+
+```typescript tab=petstore.ts
import { on } from 'mokapi'
export default function() {
on('http', (request, response) => {
- if (request.path.petId === 12) {
+ // Check if the request is for pet ID 12
+ if (request.key === '/pet/{petId}' && request.path.petId === 12) {
response.data = {
id: 12,
name: 'Garfield',
@@ -56,68 +116,245 @@ export default function() {
id: 3,
name: 'Cats'
},
- photoUrls: []
+ photoUrls: [],
+ status: 'available'
}
}
+ // Other pet IDs will receive auto-generated data
+ })
+}
+```
+
+**Step 2:** Start Mokapi with both the spec and your script
+
+```bash
+mokapi https://petstore3.swagger.io/api/v3/openapi.json /path/to/petstore.ts
+```
+
+**Step 3:** Test the result
+
+```bash
+# Request pet 12 - returns your custom "Garfield" response
+curl http://localhost/api/v3/pet/12
+
+# Request pet 99 - returns auto-generated random data
+curl http://localhost/api/v3/pet/99
+```
+
+**Before (without script):**
+```json
+{"id": 12, "name": "RandomName", "category": {"id": 1, "name": "Dogs"}, ...}
+```
+
+**After (with script):**
+```json
+{"id": 12, "name": "Garfield", "category": {"id": 3, "name": "Cats"}, ...}
+```
+
+
+### Testing Error Handling
+
+**Scenario:** Your application needs to handle `404 Not Found` responses gracefully.
+
+With Mokapi Scripts, you can customize responses for specific test scenarios.
+
+```javascript tab=script.js
+import { on } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ if (request.path.petId === 999) {
+ response.statusCode = 404
+ }
+ })
+}
+```
+
+To run the script, pass it to Mokapi when starting the server:
+
+```bash
+mokapi https://petstore3.swagger.io/api/v3/openapi.json ./script.js
+```
+
+You can test the behavior of your mocked API with the request:
+
+```bash
+curl http://localhost/api/v3/pet/999
+```
+
+### Simulating Network Delays
+
+**Scenario:** Test how your app behaves with slow API responses.
+
+Use Mokapi Scripts to add latency:
+
+```javascript tab=script.js
+import { on, sleep } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ if (request.path.petId === 999) {
+ // delay the response by 5 seconds
+ sleep('5s');
+ }
+ })
+}
+```
+
+### Testing Different Data States
+
+**Scenario:** Verify your UI displays different pet statuses correctly.
+
+Mokapi generates varied data on each request - refresh to see different values for enums and optional fields.
+Set specific responses for petId 1 and 2 and for all others Mokapi will respond with random data
+
+```javascript tab=script.js
+import { on, sleep } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ switch (request.path.petId) {
+ case 1: // Note: path parameter petId is defined as integer in spec
+ response.data = {
+ id: 1,
+ name: 'Max',
+ photoUrls: []
+ }
+ return
+ case 2:
+ response.data = {
+ id: 2,
+ name: 'Bella',
+ photoUrls: []
+ }
+ return
+ }
})
}
```
-Start the mock server with the following command, referencing both the API specification and your custom script:
+### Example: Stateful Interactions
-```bash tab=CLI
-mokapi --providers-http-url https://petstore3.swagger.io/api/v3/openapi.json --providers-file-filename /path/to/petstore.ts
+Simulate creating and retrieving a pet:
+
+```typescript
+import { on } from 'mokapi'
+
+let createdPets = new Map()
+
+export default function() {
+ on('http', (request, response) => {
+ // Handle POST /pet (create)
+ if (request.key === '/pet/{petId}' && request.method === 'POST') {
+ const newPet = request.body
+ createdPets.set(newPet.id, newPet)
+ response.statusCode = 201
+ response.data = newPet
+ }
+
+ // Handle GET /pet/{petId} (retrieve)
+ if (request.key === '/pet/{petId}' && request.method === 'GET' && createdPets.has(request.path.petId)) {
+ response.data = createdPets.get(request.path.petId)
+ }
+ })
+}
```
-Now, when you visit [http://localhost/api/v3/pet/12](http://localhost/api/v3/pet/12), Mokapi will return your custom-defined response for Garfield. Requests for other pet IDs will still generate random data based on the API specification.
+## Understanding Request Matching
-For further details on creating dynamic data, see [Test-Data](/docs/get-started/test-data.md).
+When writing Mokapi Scripts, you'll often need to identify which API endpoint was called.
-## Swagger 2.0 support
+### Using `request.key`
-Mokapi supports the Swagger 2.0 specification, making it easy to work with older API definitions. When you provide a Swagger 2.0 file, Mokapi automatically converts it to OpenAPI 3.0, ensuring compatibility and consistent response generation. This seamless conversion allows you to benefit from OpenAPI 3.0 features while using your existing Swagger 2.0 specifications. Keep this in mind when referencing elements from a Swagger 2.0 file, as the structure and syntax might differ slightly in OpenAPI 3.0.
+The `request.key` property contains the path pattern from your OpenAPI specification:
+```typescript
+// OpenAPI spec defines: /pet/{petId}
+// User requests: http://localhost/api/v3/pet/12
-### Example: Referencing a Schema from Swagger 2.0
+on('http', (request, response) => {
+ console.log(request.key) // "/pet/{petId}"
+ console.log(request.url.path) // "/api/v3/pet/12"
+ console.log(request.operationId) // "getPetById" defined in OpenAPI spec
+})
+```
-Here is a simple Swagger 2.0 specification file:
+**Best Practice:** Always check `request.key` to match the correct endpoint:
+```typescript
+// ✅ Reliable - matches the OpenAPI path pattern
+if (request.key === '/pet/{petId}') {
+ // Your logic here
+}
+// ❌ Fragile - breaks if base path changes
+if (request.url.path.startsWith('/api/v3/pet/')) {
+ // Your logic here
+}
+```
+
+## Working with Swagger 2.0
+
+Mokapi fully supports Swagger 2.0 specifications. When you provide a Swagger 2.0 file,
+Mokapi automatically converts it to OpenAPI 3.0 internally.
+
+### Schema Reference Translation
+
+**Swagger 2.0:**
```yaml
-swagger: '2.0'
-info:
- title: A Swagger 2.0 specification file
- version: 1.0.0
-paths:
- /pets:
- get:
- responses:
- '200':
- description: A list of pets.
- schema:
- $ref: '#/definitions/Pet'
-definitions:
+definitions:
Pet:
type: object
```
-In OpenAPI 3.0, you can reference the Pet schema from the Swagger 2.0 specification file as shown below:
-
+**OpenAPI 3.0 (how Mokapi sees it):**
```yaml
-openapi: '3.0'
-info:
- title: A OpenAPI 3.0 specification file
- version: 1.0.0
-paths:
- /pets:
- get:
- responses:
- '200':
- description: A list of pets.
- content:
- application/json:
- schema:
- $ref: 'path/to/swagger.yaml#/components/schemas/Pet'
-```
-
-Swagger 2.0 uses #/definitions for internal schemas, while OpenAPI 3.0 utilizes #/components/schemas. However, Mokapi automatically resolves these differences for you.
-
-With Mokapi's support for Swagger 2.0, you can modernize your API testing and mocking processes without abandoning your existing specifications. Whether you're transitioning to OpenAPI 3.0 or maintaining legacy systems, Mokapi makes the process seamless and efficient.
\ No newline at end of file
+components:
+ schemas:
+ Pet:
+ type: object
+```
+
+Mokapi handles this translation automatically - you don't need to modify your specs.
+
+### What This Means for You
+
+**✅ Your Swagger 2.0 specs work immediately** - no conversion needed
+**✅ Reference resolution handled automatically** - Mokapi translates between formats
+**✅ Schema paths differ** - Mokapi transforms the path
+
+## Best Practices
+
+### ✅ Start with Auto-Generated Data
+
+Let Mokapi handle the basics. Adjust only data for you specific need.
+
+```javascript
+import { on, sleep } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ if (request.key === '/pet/{petId}' && request.path.petId === 10) {
+ // Instead of replacing entire responses, you can modify specific fields:
+ response.data.name = 'Garfield'
+ }
+ })
+}
+```
+
+### ✅ Version Your Specifications
+
+Keep your OpenAPI specs and Mokapi Scripts in version control alongside your code.
+
+### ✅ Use Scripts for Edge Cases
+
+Focus your scripts on:
+- Error scenarios (404, 500, validation errors)
+- Authentication/authorization testing
+- Specific business logic you want to test
+- Stateful workflows
+
+## Next Steps
+
+**Ready for more advanced features?**
+
+- [Test Data Generation](/docs/get-started/test-data.md) - Create realistic, varied test data
+- [Mokapi Scripts Guide](/docs/javascript-api/overview) - Full scripting reference and examples
From d0153b2ee83001135beaab0a8b1d5bdf71e21d65 Mon Sep 17 00:00:00 2001
From: maesi
Date: Thu, 12 Feb 2026 23:49:30 +0100
Subject: [PATCH 40/75] docs: update HTTP overview doc
---
docs/http/overview.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/http/overview.md b/docs/http/overview.md
index 359067198..87d827c0c 100644
--- a/docs/http/overview.md
+++ b/docs/http/overview.md
@@ -272,9 +272,9 @@ The `request.key` property contains the path pattern from your OpenAPI specifica
// User requests: http://localhost/api/v3/pet/12
on('http', (request, response) => {
- console.log(request.key) // "/pet/{petId}"
- console.log(request.url.path) // "/api/v3/pet/12"
+ console.log(request.key) // "/pet/{petId}"
console.log(request.operationId) // "getPetById" defined in OpenAPI spec
+ console.log(request.url.path) // "/api/v3/pet/12"
})
```
From 7721934ef9d91d5f0c6c0eaa5df593802ccf4479 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 07:43:36 +0000
Subject: [PATCH 41/75] Bump golang.org/x/text from 0.33.0 to 0.34.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.33.0 to 0.34.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.33.0...v0.34.0)
---
updated-dependencies:
- dependency-name: golang.org/x/text
dependency-version: 0.34.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index a75ab4d4d..bc9215b96 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/yuin/gopher-lua v1.1.1
golang.org/x/net v0.49.0
- golang.org/x/text v0.33.0
+ golang.org/x/text v0.34.0
gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d
gopkg.in/yaml.v3 v3.0.1
layeh.com/gopher-luar v1.0.11
diff --git a/go.sum b/go.sum
index 1d553f2a9..d613f24cb 100644
--- a/go.sum
+++ b/go.sum
@@ -213,8 +213,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
-golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
From be8a30b11107283f77eecfa9102138b046c67e12 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 07:43:54 +0000
Subject: [PATCH 42/75] Bump @playwright/test from 1.58.0 to 1.58.2 in /webui
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.58.0 to 1.58.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.58.0...v1.58.2)
---
updated-dependencies:
- dependency-name: "@playwright/test"
dependency-version: 1.58.2
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 24 ++++++++++++------------
webui/package.json | 2 +-
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 4cd9b0e02..9132c858b 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -39,7 +39,7 @@
"xml-formatter": "^3.6.7"
},
"devDependencies": {
- "@playwright/test": "^1.57.0",
+ "@playwright/test": "^1.58.2",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
@@ -887,13 +887,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.58.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
- "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.58.0"
+ "playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
@@ -5839,13 +5839,13 @@
}
},
"node_modules/playwright": {
- "version": "1.58.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
- "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.58.0"
+ "playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
@@ -5858,9 +5858,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.58.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
- "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/webui/package.json b/webui/package.json
index fdbc1bf9b..76da2d8c2 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -48,7 +48,7 @@
"xml-formatter": "^3.6.7"
},
"devDependencies": {
- "@playwright/test": "^1.57.0",
+ "@playwright/test": "^1.58.2",
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
From e3c9068473a638611e6630d8a1453172dc962128 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 07:51:12 +0000
Subject: [PATCH 43/75] Bump nodemailer from 8.0.0 to 8.0.1 in /webui
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1)
---
updated-dependencies:
- dependency-name: nodemailer
dependency-version: 8.0.1
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 8 ++++----
webui/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 9132c858b..6f56d96a9 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -29,7 +29,7 @@
"markdown-it-container": "^4.0.0",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
- "nodemailer": "^8.0.0",
+ "nodemailer": "^8.0.1",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
@@ -5284,9 +5284,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.0.tgz",
- "integrity": "sha512-xvVJf/f0bzmNpnRIbhCp/IKxaHgJ6QynvUbLXzzMRPG3LDQr5oXkYuw4uDFyFYs8cge8agwwrJAXZsd4hhMquw==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
diff --git a/webui/package.json b/webui/package.json
index 76da2d8c2..14a82e6e6 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -38,7 +38,7 @@
"markdown-it-container": "^4.0.0",
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
- "nodemailer": "^8.0.0",
+ "nodemailer": "^8.0.1",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
From 6773761ec2184d7c2becc346d822fae39b43e6e4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 07:58:56 +0000
Subject: [PATCH 44/75] Bump vue from 3.5.27 to 3.5.28 in /webui
Bumps [vue](https://github.com/vuejs/core) from 3.5.27 to 3.5.28.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.27...v3.5.28)
---
updated-dependencies:
- dependency-name: vue
dependency-version: 3.5.28
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 128 ++++++++++++++++++++--------------------
webui/package.json | 2 +-
2 files changed, 65 insertions(+), 65 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 6f56d96a9..3cc21afd5 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -30,7 +30,7 @@
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^8.0.1",
- "vue": "^3.5.27",
+ "vue": "^3.5.28",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
@@ -93,12 +93,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
- "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.6"
+ "@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -108,9 +108,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
- "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1714,39 +1714,39 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
- "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz",
+ "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.5",
- "@vue/shared": "3.5.27",
- "entities": "^7.0.0",
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.28",
+ "entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
- "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz",
+ "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.27",
- "@vue/shared": "3.5.27"
+ "@vue/compiler-core": "3.5.28",
+ "@vue/shared": "3.5.28"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
- "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz",
+ "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.5",
- "@vue/compiler-core": "3.5.27",
- "@vue/compiler-dom": "3.5.27",
- "@vue/compiler-ssr": "3.5.27",
- "@vue/shared": "3.5.27",
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.28",
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/compiler-ssr": "3.5.28",
+ "@vue/shared": "3.5.28",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
@@ -1754,13 +1754,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
- "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz",
+ "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.27",
- "@vue/shared": "3.5.27"
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/shared": "3.5.28"
}
},
"node_modules/@vue/devtools-api": {
@@ -1867,53 +1867,53 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz",
- "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz",
+ "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==",
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.27"
+ "@vue/shared": "3.5.28"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
- "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz",
+ "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.27",
- "@vue/shared": "3.5.27"
+ "@vue/reactivity": "3.5.28",
+ "@vue/shared": "3.5.28"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
- "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz",
+ "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.27",
- "@vue/runtime-core": "3.5.27",
- "@vue/shared": "3.5.27",
+ "@vue/reactivity": "3.5.28",
+ "@vue/runtime-core": "3.5.28",
+ "@vue/shared": "3.5.28",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
- "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz",
+ "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.27",
- "@vue/shared": "3.5.27"
+ "@vue/compiler-ssr": "3.5.28",
+ "@vue/shared": "3.5.28"
},
"peerDependencies": {
- "vue": "3.5.27"
+ "vue": "3.5.28"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz",
- "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz",
+ "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
"license": "MIT"
},
"node_modules/@vue/tsconfig": {
@@ -7369,16 +7369,16 @@
"license": "MIT"
},
"node_modules/vue": {
- "version": "3.5.27",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz",
- "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz",
+ "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.27",
- "@vue/compiler-sfc": "3.5.27",
- "@vue/runtime-dom": "3.5.27",
- "@vue/server-renderer": "3.5.27",
- "@vue/shared": "3.5.27"
+ "@vue/compiler-dom": "3.5.28",
+ "@vue/compiler-sfc": "3.5.28",
+ "@vue/runtime-dom": "3.5.28",
+ "@vue/server-renderer": "3.5.28",
+ "@vue/shared": "3.5.28"
},
"peerDependencies": {
"typescript": "*"
diff --git a/webui/package.json b/webui/package.json
index 14a82e6e6..a42b163c8 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -39,7 +39,7 @@
"mime-types": "^3.0.2",
"ncp": "^2.0.0",
"nodemailer": "^8.0.1",
- "vue": "^3.5.27",
+ "vue": "^3.5.28",
"vue-router": "^5.0.2",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
From 666cfa4bdbde242f028a114c43535f1b82473754 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 08:06:49 +0000
Subject: [PATCH 45/75] Bump @types/node from 25.2.1 to 25.2.3 in /webui
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.1 to 25.2.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
---
updated-dependencies:
- dependency-name: "@types/node"
dependency-version: 25.2.3
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
webui/package-lock.json | 8 ++++----
webui/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 3cc21afd5..e75caf5d0 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -43,7 +43,7 @@
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
- "@types/node": "^25.2.1",
+ "@types/node": "^25.2.3",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
@@ -1374,9 +1374,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.2.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
- "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
+ "version": "25.2.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
+ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
diff --git a/webui/package.json b/webui/package.json
index a42b163c8..bcc620d7c 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -52,7 +52,7 @@
"@rushstack/eslint-patch": "^1.15.0",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
- "@types/node": "^25.2.1",
+ "@types/node": "^25.2.3",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
From 364d42cd64b81e4ab113f9ebad5a45874c7487df Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 13 Feb 2026 10:51:35 +0100
Subject: [PATCH 46/75] feat(webui): scroll sidebar navigation to the current
page
---
webui/src/components/docs/DocNav.vue | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/webui/src/components/docs/DocNav.vue b/webui/src/components/docs/DocNav.vue
index 528f49f14..6f6e5f2e0 100644
--- a/webui/src/components/docs/DocNav.vue
+++ b/webui/src/components/docs/DocNav.vue
@@ -1,7 +1,7 @@
-
+ {{ root.label }}
From 91c101d9d15d861af5d497670f43eb00817004b3 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 13 Feb 2026 10:52:54 +0100
Subject: [PATCH 47/75] feat(javascript): add new module for working with files
---
docs/config.json | 20 ++++
.../mokapi-file/append-string.md | 28 +++++
docs/javascript-api/mokapi-file/read.md | 30 +++++
.../mokapi-file/write-string.md | 27 +++++
docs/javascript-api/overview.md | 8 ++
engine/common/host.go | 2 +
engine/enginetest/host.go | 14 +++
engine/host.go | 12 +-
js/file/file.go | 111 +++++++++++++++++-
js/file/file_test.go | 63 ++++++++++
js/script.go | 1 +
11 files changed, 309 insertions(+), 7 deletions(-)
create mode 100644 docs/javascript-api/mokapi-file/append-string.md
create mode 100644 docs/javascript-api/mokapi-file/read.md
create mode 100644 docs/javascript-api/mokapi-file/write-string.md
diff --git a/docs/config.json b/docs/config.json
index 7a035b2d4..835bcb454 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -438,6 +438,26 @@
"path": "/docs/javascript-api/mokapi-encoding/base64-decode"
}
]
+ },
+ {
+ "label": "mokapi/file",
+ "items": [
+ {
+ "label": "read",
+ "source": "javascript-api/mokapi-file/read.md",
+ "path": "/docs/javascript-api/mokapi-file/read"
+ },
+ {
+ "label": "writeString",
+ "source": "javascript-api/mokapi-file/write-string.md",
+ "path": "/docs/javascript-api/mokapi-file/write-string"
+ },
+ {
+ "label": "appendString",
+ "source": "javascript-api/mokapi-file/append-string.md",
+ "path": "/docs/javascript-api/mokapi-file/append-string"
+ }
+ ]
}
]
}
diff --git a/docs/javascript-api/mokapi-file/append-string.md b/docs/javascript-api/mokapi-file/append-string.md
new file mode 100644
index 000000000..536332f14
--- /dev/null
+++ b/docs/javascript-api/mokapi-file/append-string.md
@@ -0,0 +1,28 @@
+---
+title: appendString( path, s )
+description: Appends a string to a file at the given path.
+---
+# appendString( path, s )
+
+Appends the string `s` to a file at the given path.
+
+| Parameter | Type | Description |
+|-----------|--------|------------------------------|
+| path | string | Path to the file to write |
+| s | string | The string content to append |
+
+If the path is relative, Mokapi resolves it relative to the **entry script file**.
+
+If the file does not exist, it will be created. If it exists, the string will be appended.
+
+## Example Appending File
+
+```javascript
+import { appendString, writeString, read } from 'mokapi/file'
+
+export default function() {
+ writeString('data.json', 'Hello World')
+ appendString('data.json', '!')
+ console.log(read('data.json'))
+}
+```
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi-file/read.md b/docs/javascript-api/mokapi-file/read.md
new file mode 100644
index 000000000..aebb4076e
--- /dev/null
+++ b/docs/javascript-api/mokapi-file/read.md
@@ -0,0 +1,30 @@
+---
+title: read( path )
+description: Reads the contents of a file and returns it as a string.
+---
+# read( path )
+
+Reads the file at the given path until EOF and returns its contents.
+
+| Parameter | Type | Description |
+|-----------|--------|---------------------------|
+| path | string | Path to the file to read |
+
+If the path is relative, Mokapi resolves it relative to the **entry script file**.
+
+## Returns
+
+| Type | Description |
+|--------|-------------------------|
+| string | The content of the file |
+
+## Example Reading File
+
+```javascript
+import { read } from 'mokapi/file'
+
+export default function() {
+ const data = read('data.json')
+ console.log(data)
+}
+```
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi-file/write-string.md b/docs/javascript-api/mokapi-file/write-string.md
new file mode 100644
index 000000000..a94118724
--- /dev/null
+++ b/docs/javascript-api/mokapi-file/write-string.md
@@ -0,0 +1,27 @@
+---
+title: writeString( path, s )
+description: Writes a string to a file at the given path.
+---
+# writeString( path, s )
+
+Writes the string `s` to a file at the given path.
+
+| Parameter | Type | Description |
+|-----------|--------|-------------------------------|
+| path | string | Path to the file to write |
+| s | string | The string content to write |
+
+If the path is relative, Mokapi resolves it relative to the **entry script file**.
+
+If the file does not exist, it will be created. If it exists, it will be overwritten.
+
+## Example Writing File
+
+```javascript
+import { writeString, read } from 'mokapi/file'
+
+export default function() {
+ writeString('data.json', 'Hello World')
+ console.log(read('data.json'))
+}
+```
\ No newline at end of file
diff --git a/docs/javascript-api/overview.md b/docs/javascript-api/overview.md
index 76e5a2b32..15178b680 100644
--- a/docs/javascript-api/overview.md
+++ b/docs/javascript-api/overview.md
@@ -232,7 +232,15 @@ Functions for encoding and decoding data.
| [base64.encode( input )](/docs/javascript-api/mokapi-encoding/base64-encode.md) | Encodes a string to Base64. |
| [base64.decode( input )](/docs/javascript-api/mokapi-encoding/base64-decode.md) | Decodes a Base64 string. |
+### mokapi/file
+Functions for working with files
+
+| Functions | Description |
+|------------------------------------------------------------------------------|------------------------------------------------|
+| [read( path )](/docs/javascript-api/mokapi-file/read.md) | Reads the contents of a file. |
+| [writeString( path, s )](/docs/javascript-api/mokapi-file/write-string.md) | Writes a string to a file at the given path. |
+| [appendString( path, s )](/docs/javascript-api/mokapi-file/append-string.md) | Appends a string to a file at the given path. |
diff --git a/engine/common/host.go b/engine/common/host.go
index e1df87c2e..d9fb944ad 100644
--- a/engine/common/host.go
+++ b/engine/common/host.go
@@ -49,6 +49,8 @@ type Host interface {
Unlock()
Store() Store
+
+ Cwd() string
}
type Logger interface {
diff --git a/engine/enginetest/host.go b/engine/enginetest/host.go
index 2a72cdf3f..384e23730 100644
--- a/engine/enginetest/host.go
+++ b/engine/enginetest/host.go
@@ -8,6 +8,8 @@ import (
"mokapi/schema/json/generator"
"net/http"
"net/url"
+ "os"
+ "path/filepath"
"sync"
)
@@ -31,6 +33,7 @@ type Host struct {
FindFakerNodeFunc func(name string) *generator.Node
m sync.Mutex
StoreTest *engine.Store
+ CwdFunc func() string
}
type HttpClient struct {
@@ -183,6 +186,17 @@ func (h *Host) AddCleanupFunc(f func()) {
h.CleanupFuncs = append(h.CleanupFuncs, f)
}
+func (h *Host) Cwd() string {
+ if h.CwdFunc != nil {
+ return h.CwdFunc()
+ }
+ ex, err := os.Executable()
+ if err != nil {
+ panic(err)
+ }
+ return filepath.Dir(ex)
+}
+
func mustParse(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
diff --git a/engine/host.go b/engine/host.go
index 6b8d21d93..546e79d82 100644
--- a/engine/host.go
+++ b/engine/host.go
@@ -284,8 +284,8 @@ func (sh *scriptHost) OpenFile(path string, hint string) (*dynamic.Config, error
if len(hint) > 0 {
path = filepath.Join(hint, path)
} else {
- p := getScriptPath(sh.file.Info.Kernel().Url)
- path = filepath.Join(filepath.Dir(p), path)
+ cwd := sh.Cwd()
+ path = filepath.Join(cwd, path)
}
}
@@ -346,6 +346,14 @@ func (sh *scriptHost) Store() common.Store {
return sh.engine.store
}
+func (sh *scriptHost) Cwd() string {
+ u := sh.file.Info.Kernel().Url
+ if len(u.Path) > 0 {
+ return filepath.Dir(u.Path)
+ }
+ return filepath.Dir(u.Opaque)
+}
+
func getScriptPath(u *url.URL) string {
if len(u.Path) > 0 {
return u.Path
diff --git a/js/file/file.go b/js/file/file.go
index b52e06a64..30a9201c7 100644
--- a/js/file/file.go
+++ b/js/file/file.go
@@ -2,8 +2,14 @@ package file
import (
"encoding/json"
+ "fmt"
+ "io"
"mokapi/config/dynamic"
+ "mokapi/config/dynamic/provider/file"
"mokapi/engine/common"
+ "net/url"
+ "os"
+ "path/filepath"
"github.com/dop251/goja"
)
@@ -23,17 +29,30 @@ func Enable(rt *goja.Runtime, host common.Host, parent *dynamic.Config) {
_ = rt.Set("open", r.open)
}
-func (o *Module) open(file string, args map[string]interface{}) (any, error) {
- f, err := o.host.OpenFile(file, "")
+func Require(vm *goja.Runtime, module *goja.Object) {
+ o := vm.Get("mokapi/internal").(*goja.Object)
+ host := o.Get("host").Export().(common.Host)
+ m := &Module{
+ rt: vm,
+ host: host,
+ }
+ obj := module.Get("exports").(*goja.Object)
+ _ = obj.Set("read", m.Read)
+ _ = obj.Set("writeString", m.WriteString)
+ _ = obj.Set("appendString", m.AppendString)
+}
+
+func (m *Module) open(file string, args map[string]interface{}) (any, error) {
+ f, err := m.host.OpenFile(file, "")
if err != nil {
return "", err
}
- dynamic.AddRef(o.parent, f)
+ dynamic.AddRef(m.parent, f)
switch args["as"] {
case "binary":
return f.Raw, nil
case "resolved":
- return o.resolve(f)
+ return m.resolve(f)
case "string":
fallthrough
default:
@@ -41,7 +60,7 @@ func (o *Module) open(file string, args map[string]interface{}) (any, error) {
}
}
-func (o *Module) resolve(f *dynamic.Config) (any, error) {
+func (m *Module) resolve(f *dynamic.Config) (any, error) {
b, err := json.Marshal(f.Data)
if err != nil {
return nil, err
@@ -50,3 +69,85 @@ func (o *Module) resolve(f *dynamic.Config) (any, error) {
err = json.Unmarshal(b, &v)
return v, err
}
+
+func (m *Module) Read(path string) (string, error) {
+ p, err := m.resolvePath(path)
+ if err != nil {
+ panic(fmt.Sprintf("failed to write to file: %s", err))
+ }
+ f, err := os.Open(p)
+ if err != nil {
+ return "", err
+ }
+ defer func() { _ = f.Close() }()
+
+ b, err := io.ReadAll(f)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+func (m *Module) WriteString(path, s string) {
+ p, err := m.resolvePath(path)
+ if err != nil {
+ panic(fmt.Sprintf("failed to write to file: %s", err))
+ }
+ f, err := os.Create(p)
+ if err != nil {
+ panic(fmt.Sprintf("failed to write to file: %s", err))
+ }
+ defer func() {
+ _ = f.Close()
+ }()
+
+ _, err = f.WriteString(s)
+ if err != nil {
+ panic(fmt.Sprintf("failed to write to file: %s", err))
+ }
+}
+
+func (m *Module) AppendString(path, s string) {
+ p, err := m.resolvePath(path)
+ if err != nil {
+ panic(fmt.Sprintf("failed to write to file: %s", err))
+ }
+ f, err := os.OpenFile(p,
+ os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ panic(fmt.Sprintf("failed to append to file: %s", err))
+ }
+ defer func() {
+ _ = f.Close()
+ }()
+
+ _, err = f.WriteString(s)
+ if err != nil {
+ panic(fmt.Sprintf("failed to append string: %v", err))
+ }
+}
+
+func (m *Module) resolvePath(path string) (string, error) {
+ u, err := url.Parse(path)
+ if err != nil || len(u.Scheme) == 0 || len(u.Opaque) > 0 {
+ if !filepath.IsAbs(path) {
+ cwd := m.host.Cwd()
+ path = filepath.Join(cwd, path)
+ }
+
+ u, err = file.ParseUrl(path)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if u.Scheme != "file" {
+ return "", fmt.Errorf("file access only allowed from local scripts")
+ }
+
+ p := u.Path
+ if len(u.Opaque) > 0 {
+ p = u.Opaque
+ }
+ return p, nil
+}
diff --git a/js/file/file_test.go b/js/file/file_test.go
index 8f872b5fd..2df4d34f6 100644
--- a/js/file/file_test.go
+++ b/js/file/file_test.go
@@ -11,6 +11,8 @@ import (
"mokapi/providers/openapi/openapitest"
"mokapi/providers/openapi/schema"
"mokapi/providers/openapi/schema/schematest"
+ "os"
+ "path/filepath"
"testing"
"github.com/dop251/goja"
@@ -77,6 +79,66 @@ func TestModule_Open(t *testing.T) {
r.Equal(t, map[string]any{"description": "circular reference"}, v.Export())
},
},
+ {
+ name: "open",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ err := os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("Hello World"), 0o644)
+ r.NoError(t, err)
+
+ host.CwdFunc = func() string {
+ return dir
+ }
+
+ v, err := vm.RunString(`
+ const m = require("mokapi/file")
+ m.read('foo.txt');
+ `)
+ r.NoError(t, err)
+ r.Equal(t, "Hello World", v.Export(), dir)
+ },
+ },
+ {
+ name: "write file",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ host.CwdFunc = func() string {
+ return dir
+ }
+
+ _, err := vm.RunString(`
+ const m = require("mokapi/file")
+ m.writeString('foo.txt', 'Hello World');
+ `)
+ r.NoError(t, err)
+
+ b, err := os.ReadFile(filepath.Join(dir, "foo.txt"))
+ r.NoError(t, err)
+ r.Equal(t, "Hello World", string(b), dir)
+ },
+ },
+ {
+ name: "append string to file",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ err := os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("Hello World"), 0o644)
+ r.NoError(t, err)
+
+ host.CwdFunc = func() string {
+ return dir
+ }
+
+ _, err = vm.RunString(`
+ const m = require("mokapi/file")
+ m.appendString('foo.txt', '!');
+ `)
+ r.NoError(t, err)
+
+ b, err := os.ReadFile(filepath.Join(dir, "foo.txt"))
+ r.NoError(t, err)
+ r.Equal(t, "Hello World!", string(b), dir)
+ },
+ },
}
t.Parallel()
@@ -98,6 +160,7 @@ func TestModule_Open(t *testing.T) {
js.EnableInternal(vm, host, loop, source)
reg.Enable(vm)
file.Enable(vm, host, source)
+ reg.RegisterNativeModule("mokapi/file", file.Require)
tc.test(t, vm, host)
})
diff --git a/js/script.go b/js/script.go
index 77be59fdd..d008ea6c4 100644
--- a/js/script.go
+++ b/js/script.go
@@ -269,6 +269,7 @@ func RegisterNativeModules(registry *require.Registry) {
registry.RegisterNativeModule("mokapi/smtp", mail.Require)
registry.RegisterNativeModule("mokapi/ldap", ldap.Require)
registry.RegisterNativeModule("mokapi/encoding", encoding.Require)
+ registry.RegisterNativeModule("mokapi/file", file.Require)
}
func isClosingError(err error) bool {
From fe1624b1529b83f93e2bdc07e221f57157ecf135 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 13 Feb 2026 10:59:04 +0100
Subject: [PATCH 48/75] feat(javascript): add typescript definitions for new
module
---
npm/types/file.d.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
create mode 100644 npm/types/file.d.ts
diff --git a/npm/types/file.d.ts b/npm/types/file.d.ts
new file mode 100644
index 000000000..4862e4ce1
--- /dev/null
+++ b/npm/types/file.d.ts
@@ -0,0 +1,50 @@
+/**
+ * Reads the contents of a file and returns it as a string.
+ *
+ * If the path is relative, it is resolved relative to the entry script file.
+ *
+ * @param path - Path to the file to read.
+ * @returns The contents of the file.
+ *
+ * @example
+ * export default function() {
+ * const data = read('data.json')
+ * console.log(data)
+ * }
+ */
+export function read(path: string): string;
+
+/**
+ * Writes a string to a file at the given path.
+ *
+ * If the path is relative, it is resolved relative to the entry script file.
+ * If the file does not exist, it will be created.
+ * If the file exists, it will be overwritten.
+ *
+ * @param path - Path to the file to write.
+ * @param s - The string content to write.
+ *
+ * @example
+ * export default function() {
+ * writeString('data.json', 'Hello World')
+ * }
+ */
+export function writeString(path: string, s: string): void;
+
+/**
+ * Appends a string to a file at the given path.
+ *
+ * If the path is relative, it is resolved relative to the entry script file.
+ * If the file does not exist, it will be created.
+ * If the file exists, the string will be appended.
+ *
+ * @param path - Path to the file to append to.
+ * @param s - The string content to append.
+ *
+ * @example
+ * export default function() {
+ * writeString('data.json', 'Hello')
+ * appendString('data.json', ' World')
+ * }
+ */
+export function appendString(path: string, s: string): void;
\ No newline at end of file
From feb32760c7a86386241f242745033dafa8975174 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 16:16:58 +0000
Subject: [PATCH 49/75] Bump golang.org/x/net from 0.49.0 to 0.50.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)
---
updated-dependencies:
- dependency-name: golang.org/x/net
dependency-version: 0.50.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
go.mod | 6 +++---
go.sum | 16 ++++++++--------
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/go.mod b/go.mod
index bc9215b96..15c0da40a 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
github.com/yuin/gopher-lua v1.1.1
- golang.org/x/net v0.49.0
+ golang.org/x/net v0.50.0
golang.org/x/text v0.34.0
gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d
gopkg.in/yaml.v3 v3.0.1
@@ -80,8 +80,8 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
- golang.org/x/crypto v0.47.0 // indirect
- golang.org/x/sys v0.40.0 // indirect
+ golang.org/x/crypto v0.48.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
diff --git a/go.sum b/go.sum
index d613f24cb..64ce940bf 100644
--- a/go.sum
+++ b/go.sum
@@ -190,13 +190,13 @@ go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
-golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
-golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -207,11 +207,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
-golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
-golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
From 824093a7896747577efadde916c89eedfabef721 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 16:24:46 +0000
Subject: [PATCH 50/75] Bump github.com/go-git/go-git/v5 from 5.16.4 to 5.16.5
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.4 to 5.16.5.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.16.4...v5.16.5)
---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
dependency-version: 5.16.5
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 15c0da40a..962b84143 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/evanw/esbuild v0.27.3
github.com/fsnotify/fsnotify v1.9.0
github.com/go-co-op/gocron v1.37.0
- github.com/go-git/go-git/v5 v5.16.4
+ github.com/go-git/go-git/v5 v5.16.5
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0
diff --git a/go.sum b/go.sum
index 64ce940bf..6a4a75f21 100644
--- a/go.sum
+++ b/go.sum
@@ -95,8 +95,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
-github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
+github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
+github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
From 51ee813b2de1070a307ca00fcc4a895bcf601cda Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 13 Feb 2026 18:58:52 +0100
Subject: [PATCH 51/75] doc: update doc
---
docs/http/overview.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/http/overview.md b/docs/http/overview.md
index 87d827c0c..38163e223 100644
--- a/docs/http/overview.md
+++ b/docs/http/overview.md
@@ -317,13 +317,13 @@ Mokapi handles this translation automatically - you don't need to modify your sp
### What This Means for You
-**✅ Your Swagger 2.0 specs work immediately** - no conversion needed
-**✅ Reference resolution handled automatically** - Mokapi translates between formats
-**✅ Schema paths differ** - Mokapi transforms the path
+✅ Your Swagger 2.0 specs work immediately - no conversion needed
+✅ Reference resolution handled automatically - Mokapi translates between formats
+✅ Schema paths differ - Mokapi transforms the path
## Best Practices
-### ✅ Start with Auto-Generated Data
+### Start with Auto-Generated Data
Let Mokapi handle the basics. Adjust only data for you specific need.
@@ -340,11 +340,11 @@ export default function() {
}
```
-### ✅ Version Your Specifications
+### Version Your Specifications
Keep your OpenAPI specs and Mokapi Scripts in version control alongside your code.
-### ✅ Use Scripts for Edge Cases
+### Use Scripts for Edge Cases
Focus your scripts on:
- Error scenarios (404, 500, validation errors)
From a171f5779f9ae87df6245eab6f5a3974aa256b49 Mon Sep 17 00:00:00 2001
From: maesi
Date: Fri, 13 Feb 2026 20:15:17 +0100
Subject: [PATCH 52/75] fix(webui): fix heading hierarchy and set max-width for
normal text
---
webui/src/assets/home.css | 7 +++-
webui/src/views/Home.vue | 45 +++++++++++-----------
webui/src/views/Http.vue | 16 ++++----
webui/src/views/Kafka.vue | 24 ++++++------
webui/src/views/Ldap.vue | 18 ++++-----
webui/src/views/Mail.vue | 78 +++++++++++++++++++--------------------
6 files changed, 96 insertions(+), 92 deletions(-)
diff --git a/webui/src/assets/home.css b/webui/src/assets/home.css
index b14123efb..9a7216219 100644
--- a/webui/src/assets/home.css
+++ b/webui/src/assets/home.css
@@ -15,7 +15,7 @@
}
.home h3 {
line-height: 1.6;
- font-size: 1.5rem;
+ font-size: 1.3rem;
}
.home .hero-title {
margin-top: 5rem;
@@ -208,4 +208,9 @@ section.feature button {
.home img:not(.no-dialog) {
cursor: pointer;
+}
+
+.home .text {
+ max-width: 750px;
+ margin-inline: auto;
}
\ No newline at end of file
diff --git a/webui/src/views/Home.vue b/webui/src/views/Home.vue
index e3dcc3ff6..c32c8eb72 100644
--- a/webui/src/views/Home.vue
+++ b/webui/src/views/Home.vue
@@ -103,7 +103,6 @@ function showImage(evt: MouseEvent) {
-
@@ -135,32 +134,32 @@ function showImage(evt: MouseEvent) {
-
Why Teams Use Mokapi
-
+
Why Teams Use Mokapi
+
Mokapi helps teams move faster by removing external dependencies from development and testing.
-
Develop Without Waiting
-
+
Develop Without Waiting
+
Mock HTTP APIs, Kafka topics, LDAP directories, or mail servers
so development never blocks on missing or unstable systems.
-
Test Real Workflows
-
+
Test Real Workflows
+
Simulate realistic system behavior across protocols
and validate integrations with confidence.
-
Automate Everywhere
-
+
Automate Everywhere
+
Run Mokapi locally, in CI pipelines, or test environments
to automate API testing and speed up feedback loops.
@@ -175,7 +174,7 @@ function showImage(evt: MouseEvent) {
Mokapi helps teams move quickly without sacrificing confidence or stability.
-
+
By mocking and simulating APIs across protocols, you can automate tests,
reduce flaky integrations, and deliver reliable software — even when
external systems are unavailable or evolving.
@@ -183,23 +182,23 @@ function showImage(evt: MouseEvent) {
-
+
-
Mock More Than Just HTTP
-
+
Mock More Than Just HTTP
+
Mokapi supports multiple protocols, allowing you to test complete systems —
not just individual REST endpoints.
-
+
-
Mock REST APIs
+
Mock REST APIs
Simulate REST endpoints to develop and test clients
without waiting for real backend services.
@@ -219,7 +218,7 @@ function showImage(evt: MouseEvent) {
-
Simulate Kafka Events
+
Simulate Kafka Events
Mock Kafka topics and message streams to test
event-driven systems and service interactions.
@@ -238,7 +237,7 @@ function showImage(evt: MouseEvent) {
-
Mock LDAP Services
+
Mock LDAP Services
Simulate directory and authentication services
to test user access, roles, and permissions safely.
@@ -257,7 +256,7 @@ function showImage(evt: MouseEvent) {
-
SMTP Email Testing
+
SMTP Email Testing
Test email workflows by simulating SMTP and IMAP servers
without sending real messages.
@@ -278,12 +277,12 @@ function showImage(evt: MouseEvent) {
Built for Reliable Development and Testing
-
+
Mocking APIs across protocols is only the beginning.
Mokapi is designed to help teams prevent bugs, reduce external dependencies,
and create stable development and test environments.
-
+
This is made possible through powerful core features —
including JavaScript-based logic, configuration patching,
observability, and realistic data generation.
@@ -295,7 +294,7 @@ function showImage(evt: MouseEvent) {
Core Features
-
+
Powerful capabilities that make Mokapi flexible, controllable, and reliable in any environment.
@@ -432,7 +431,7 @@ function showImage(evt: MouseEvent) {
Use Cases & Tutorials
-
+
Explore practical ways to mock APIs and services across protocols. Mokapi fits seamlessly in local development, CI pipelines, or cloud environments.
@@ -514,7 +513,7 @@ function showImage(evt: MouseEvent) {
See Mokapi in Action
-
Explore how easily you can mock APIs and generate realistic data for testing—no backend required.
+
Explore how easily you can mock APIs and generate realistic data for testing
diff --git a/webui/src/views/Http.vue b/webui/src/views/Http.vue
index 63cacbf31..abaa35bb3 100644
--- a/webui/src/views/Http.vue
+++ b/webui/src/views/Http.vue
@@ -106,7 +106,7 @@ function getConsoleContent() {
Why Mock HTTP and REST APIs?
-
+
Modern applications depend on many external services.
Mokapi lets you mock HTTP and OpenAPI-based APIs so development
and testing are never blocked by missing or unstable backends.
@@ -120,18 +120,18 @@ function getConsoleContent() {
-
Mock APIs with OpenAPI
+
Mock APIs with OpenAPI
Instantly spin up REST API mocks from OpenAPI specifications with validation and auto-generated responses.
-
Dynamic Responses with Scripts
+
Dynamic Responses with Scripts
Use Mokapi Scripts to simulate logic, conditional flows, or edge cases without coding a backend.
-
CI/CD Ready
-
Integrate API mocks directly into your pipelines—speed up tests, reduce flakiness, and keep development moving.
+
CI/CD Ready
+
Integrate API mocks directly into your pipelines to accelerate testing, reduce instability, and drive development forward.
@@ -140,7 +140,7 @@ function getConsoleContent() {
Explore Mokapi Features
-
+
Advanced capabilities that help you scale, automate, and debug HTTP API testing.
@@ -200,7 +200,7 @@ function getConsoleContent() {
See Mokapi in Action
-
Go beyond static mocks—customize, debug, and explore your APIs with powerful built-in tools.
+
Go beyond static mocks—customize, debug, and explore your APIs with powerful built-in tools.
@@ -239,7 +239,7 @@ function getConsoleContent() {
Quick Demo
-
+
Spin up a fully working HTTP mock from an OpenAPI spec with a single command.
diff --git a/webui/src/views/Kafka.vue b/webui/src/views/Kafka.vue
index aa9971c30..25d96ffd4 100644
--- a/webui/src/views/Kafka.vue
+++ b/webui/src/views/Kafka.vue
@@ -77,7 +77,7 @@ function showImage(evt: MouseEvent) {
Kafka-based systems are difficult to test reliably.
-
+
Real Kafka clusters are heavy, slow to spin up, and hard to control in tests.
Mokapi removes this complexity by simulating Kafka behavior without brokers,
Zookeeper, or infrastructure setup.
@@ -91,17 +91,17 @@ function showImage(evt: MouseEvent) {
-
1. Define Topics
+
1. Define Topics
Describe topics and message schemas using AsyncAPI.
-
2. Run Mokapi
+
2. Run Mokapi
Start Mokapi locally or in CI to simulate Kafka producers and consumers.
-
3. Test & Observe
+
3. Test & Observe
Validate messages and inspect traffic in real time.
@@ -115,7 +115,7 @@ function showImage(evt: MouseEvent) {
-
AsyncAPI-Based Topics
+
AsyncAPI-Based Topics
Define Kafka topics and payloads declaratively using AsyncAPI,
ensuring your mocks stay aligned with production contracts.
@@ -124,7 +124,7 @@ function showImage(evt: MouseEvent) {
-
Message Validation
+
Message Validation
Catch invalid messages early by validating payloads against JSON Schema
or Avro before they reach real consumers.
@@ -133,7 +133,7 @@ function showImage(evt: MouseEvent) {
-
CI-Friendly Testing
+
CI-Friendly Testing
Replace fragile Kafka test setups with fast, deterministic simulations
in your CI/CD pipelines.
@@ -152,7 +152,7 @@ function showImage(evt: MouseEvent) {
-
Test Microservices
+
Test Microservices
Simulate incoming Kafka events to verify how services react to
different message types and edge cases.
@@ -169,7 +169,7 @@ function showImage(evt: MouseEvent) {
-
Validate Producers
+
Validate Producers
Ensure producer applications emit valid messages before they reach
downstream consumers.
@@ -186,7 +186,7 @@ function showImage(evt: MouseEvent) {
-
Simulate Workflows
+
Simulate Workflows
Mock complex message flows across multiple topics to test
end-to-end event-driven systems.
@@ -207,7 +207,7 @@ function showImage(evt: MouseEvent) {
Inspect Kafka Traffic in Real Time
-
+
Visualize topics, messages, and consumer activity using Mokapi’s dashboard.
@@ -222,7 +222,7 @@ function showImage(evt: MouseEvent) {
Test Kafka Without Running Kafka
-
+
Mock topics, validate events, and test distributed systems faster.
diff --git a/webui/src/views/Ldap.vue b/webui/src/views/Ldap.vue
index 5255db2b8..38d578b92 100644
--- a/webui/src/views/Ldap.vue
+++ b/webui/src/views/Ldap.vue
@@ -87,10 +87,10 @@ function showImage(evt: MouseEvent) {
Why Mock LDAP Directory Services?
-
+
LDAP is critical for authentication, but difficult to test reliably.
-
+
Mokapi lets you simulate directory services, authentication flows,
and edge cases without setting up or maintaining a real LDAP server.
@@ -100,7 +100,7 @@ function showImage(evt: MouseEvent) {
What You Can Do With Mokapi LDAP
-
+
Supports full LDAP operations including authentication, queries,
and directory modifications.
@@ -108,7 +108,7 @@ function showImage(evt: MouseEvent) {
-
Test Authentication Flows
+
Test Authentication Flows
Validate login flows exactly as your application expects,
including credentials, group membership, and permissions.
@@ -116,7 +116,7 @@ function showImage(evt: MouseEvent) {
-
Simulate Directory Operations
+
Simulate Directory Operations
Mock realistic directory interactions such as searches,
updates, and entry management to match real-world usage.
@@ -124,7 +124,7 @@ function showImage(evt: MouseEvent) {
-
Control Behavior & Edge Cases
+
Control Behavior & Edge Cases
Simulate failures, invalid credentials, latency, and custom responses
to test how your system behaves under stress.
@@ -269,7 +269,7 @@ function showImage(evt: MouseEvent) {
Inspect Authentication Flows
-
+
Understand exactly how clients interact with your LDAP mock.
@@ -283,10 +283,10 @@ function showImage(evt: MouseEvent) {
Define Realistic LDAP Data with LDIF
-
+
Define users, groups, and attributes using standard LDIF files.
-
+
Reuse existing directory data and mirror production-like structures without manual setup.
diff --git a/webui/src/views/Mail.vue b/webui/src/views/Mail.vue
index d3af4b9ff..e01eb7d8d 100644
--- a/webui/src/views/Mail.vue
+++ b/webui/src/views/Mail.vue
@@ -92,7 +92,7 @@ function showImage(evt: MouseEvent) {
Why Mocking Email Matters
-
+
Email is often the last untested part of an application.
Mokapi lets you validate email flows just like any other API or message stream.
@@ -106,7 +106,7 @@ function showImage(evt: MouseEvent) {
-
Mock SMTP & IMAP
+
Mock SMTP & IMAP
Simulate outgoing and incoming mail to test real user flows without external dependencies.
@@ -117,7 +117,7 @@ function showImage(evt: MouseEvent) {
-
Forward Emails Safely
+
Forward Emails Safely
Send emails to a specific test address instead of the real recipient while preserving all content.
@@ -128,7 +128,7 @@ function showImage(evt: MouseEvent) {
-
CI/CD Friendly
+
CI/CD Friendly
Validate critical workflows automatically on every commit or release.
Reduce flaky tests and prevent deployment of broken email functionality.
@@ -139,6 +139,39 @@ function showImage(evt: MouseEvent) {
+
+
+
+
+
Set Up a Fake SMTP Server in Seconds
+
Configure inboxes, routing, and forwarding with a simple file or script. No infrastructure required.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Core Mail Mocking Features
@@ -240,46 +273,13 @@ function showImage(evt: MouseEvent) {
-
-
-
-
-
-
Set Up a Fake SMTP Server in Seconds
-
Configure inboxes, routing, and forwarding with a simple file or script. No infrastructure required.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Inspect and Debug Sent Emails
-
+
View captured messages, headers, and attachments directly in Mokapi’s dashboard for fast debugging.
@@ -291,7 +291,7 @@ function showImage(evt: MouseEvent) {
Mock Email Like Any Other Dependency
-
+
Test full email workflows without external mail servers.
Get Started
From 4e1ab7377524f4ab55ccd3824ecdb196840d4853 Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 22:32:19 +0100
Subject: [PATCH 53/75] doc: fix typo
---
.../blogs/ensuring-api-contract-compliance-with-mokapi.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md b/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md
index 27c6aad45..ee78efedd 100644
--- a/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md
+++ b/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md
@@ -111,7 +111,7 @@ export default async function () {
return `https://backend1.example.com${request.url.path}?${request.url.query}`;
}
case 'backend-2': {
- return `https://backend1.example.com${request.url.path}?${request.url.query}`;
+ return `https://backend2.example.com${request.url.path}?${request.url.query}`;
}
default:
return undefined;
From 924ac30cb08e0bf4d9899b50a10f08b134d467f5 Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 22:48:46 +0100
Subject: [PATCH 54/75] refactor: error handling and add test
---
js/file/file.go | 16 +++++++------
js/file/file_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 62 insertions(+), 7 deletions(-)
diff --git a/js/file/file.go b/js/file/file.go
index 30a9201c7..6c4312d97 100644
--- a/js/file/file.go
+++ b/js/file/file.go
@@ -73,7 +73,7 @@ func (m *Module) resolve(f *dynamic.Config) (any, error) {
func (m *Module) Read(path string) (string, error) {
p, err := m.resolvePath(path)
if err != nil {
- panic(fmt.Sprintf("failed to write to file: %s", err))
+ return "", err
}
f, err := os.Open(p)
if err != nil {
@@ -88,14 +88,14 @@ func (m *Module) Read(path string) (string, error) {
return string(b), nil
}
-func (m *Module) WriteString(path, s string) {
+func (m *Module) WriteString(path, s string) error {
p, err := m.resolvePath(path)
if err != nil {
panic(fmt.Sprintf("failed to write to file: %s", err))
}
f, err := os.Create(p)
if err != nil {
- panic(fmt.Sprintf("failed to write to file: %s", err))
+ return fmt.Errorf("failed to write to file: %s", err)
}
defer func() {
_ = f.Close()
@@ -103,11 +103,12 @@ func (m *Module) WriteString(path, s string) {
_, err = f.WriteString(s)
if err != nil {
- panic(fmt.Sprintf("failed to write to file: %s", err))
+ return fmt.Errorf("failed to write to file: %s", err)
}
+ return nil
}
-func (m *Module) AppendString(path, s string) {
+func (m *Module) AppendString(path, s string) error {
p, err := m.resolvePath(path)
if err != nil {
panic(fmt.Sprintf("failed to write to file: %s", err))
@@ -115,7 +116,7 @@ func (m *Module) AppendString(path, s string) {
f, err := os.OpenFile(p,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
- panic(fmt.Sprintf("failed to append to file: %s", err))
+ return fmt.Errorf("failed to append to file: %s", err)
}
defer func() {
_ = f.Close()
@@ -123,8 +124,9 @@ func (m *Module) AppendString(path, s string) {
_, err = f.WriteString(s)
if err != nil {
- panic(fmt.Sprintf("failed to append string: %v", err))
+ return fmt.Errorf("failed to append string: %v", err)
}
+ return nil
}
func (m *Module) resolvePath(path string) (string, error) {
diff --git a/js/file/file_test.go b/js/file/file_test.go
index 2df4d34f6..5d76b5977 100644
--- a/js/file/file_test.go
+++ b/js/file/file_test.go
@@ -98,6 +98,21 @@ func TestModule_Open(t *testing.T) {
r.Equal(t, "Hello World", v.Export(), dir)
},
},
+ {
+ name: "open file not exists",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ host.CwdFunc = func() string {
+ return dir
+ }
+
+ _, err := vm.RunString(`
+ const m = require("mokapi/file")
+ m.read('foo.txt');
+ `)
+ r.ErrorContains(t, err, "foo.txt: no such file or directory")
+ },
+ },
{
name: "write file",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
@@ -117,6 +132,44 @@ func TestModule_Open(t *testing.T) {
r.Equal(t, "Hello World", string(b), dir)
},
},
+ {
+ name: "write file with error",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ host.CwdFunc = func() string {
+ return dir
+ }
+ f, err := os.OpenFile(filepath.Join(dir, "foo.txt"), os.O_RDWR|os.O_CREATE, os.ModeExclusive)
+ r.NoError(t, err)
+ defer f.Close()
+
+ _, err = vm.RunString(`
+ const m = require("mokapi/file")
+ m.writeString('foo.txt', 'Hello World');
+ `)
+ r.ErrorContains(t, err, "failed to write to file: open")
+ },
+ },
+ {
+ name: "write file catch error",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ dir := t.TempDir()
+ host.CwdFunc = func() string {
+ return dir
+ }
+ f, err := os.OpenFile(filepath.Join(dir, "foo.txt"), os.O_RDWR|os.O_CREATE, os.ModeExclusive)
+ r.NoError(t, err)
+ defer f.Close()
+
+ _, err = vm.RunString(`
+ const m = require("mokapi/file")
+ try {
+ m.writeString('foo.txt', 'Hello World');
+ } catch {}
+ `)
+ r.NoError(t, err)
+ },
+ },
{
name: "append string to file",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
From caa312a9e1a0ad8b943bb1198a07d473d0359abb Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 22:51:42 +0100
Subject: [PATCH 55/75] fix(javascript): add missing TypeScript definition
Add field 'api' to HTTP request parameter in event handler
---
npm/types/index.d.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts
index dffc8983e..42d6a7447 100644
--- a/npm/types/index.d.ts
+++ b/npm/types/index.d.ts
@@ -157,6 +157,9 @@ export interface HttpRequest {
/** Object contains querystring parameters specified by OpenAPI querystring parameters. */
readonly querystring: any;
+ /** The title of the API from the OpenAPI specification */
+ readonly api: string
+
/** Path value specified by the OpenAPI path */
readonly key: string;
From a7815210a673b6f78d5710e35b55ca15cad1b02c Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 22:58:28 +0100
Subject: [PATCH 56/75] feat(javascript): add priority to event args
Introduce a priority field that allows registering event handlers to run before or after others.
---
engine/common/host.go | 7 ++++-
engine/engine.go | 10 -------
engine/enginetest/host.go | 6 ++---
engine/event.go | 54 ++++++++++++++++++++++++++++++++++++++
engine/host.go | 40 +++++-----------------------
engine/mokapi_on_test.go | 38 +++++++++++++++++++++++++++
js/mokapi/on.go | 17 +++++++++++-
js/mokapi/on_test.go | 48 +++++++++++++++++++++++++--------
js/script.go | 2 +-
js/script_data_test.go | 6 ++---
js/script_mokapi_test.go | 24 ++++++++---------
lua/modules/mokapi.go | 2 +-
lua/modules/mokapi_test.go | 22 ++++++++--------
lua/script_test.go | 8 +++---
14 files changed, 192 insertions(+), 92 deletions(-)
create mode 100644 engine/event.go
diff --git a/engine/common/host.go b/engine/common/host.go
index d9fb944ad..b03e89a8e 100644
--- a/engine/common/host.go
+++ b/engine/common/host.go
@@ -25,6 +25,11 @@ type JobOptions struct {
Tags map[string]string
}
+type EventArgs struct {
+ Tags map[string]string
+ Priority int
+}
+
type Host interface {
Logger
SetEventLogger(func(level, message string))
@@ -35,7 +40,7 @@ type Host interface {
OpenFile(file string, hint string) (*dynamic.Config, error)
- On(event string, do EventHandler, tags map[string]string)
+ On(event string, do EventHandler, args EventArgs)
KafkaClient() KafkaClient
HttpClient(HttpClientOptions) HttpClient
diff --git a/engine/engine.go b/engine/engine.go
index 9c0512e2e..c3ca6ea86 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -103,16 +103,6 @@ func (e *Engine) AddScript(evt dynamic.ConfigEvent) error {
return nil
}
-func (e *Engine) Run(event string, args ...interface{}) []*common.Action {
- var result []*common.Action
- for _, s := range e.scripts {
- actions := s.RunEvent(event, args...)
- result = append(result, actions...)
- }
-
- return result
-}
-
func (e *Engine) Emit(event string, args ...interface{}) []*common.Action {
return e.Run(event, args...)
}
diff --git a/engine/enginetest/host.go b/engine/enginetest/host.go
index 384e23730..06c584d77 100644
--- a/engine/enginetest/host.go
+++ b/engine/enginetest/host.go
@@ -29,7 +29,7 @@ type Host struct {
KafkaClientTest *KafkaClient
EveryFunc func(every string, do func(), opt common.JobOptions)
CronFunc func(every string, do func(), opt common.JobOptions)
- OnFunc func(event string, do common.EventHandler, tags map[string]string)
+ OnFunc func(event string, do common.EventHandler, args common.EventArgs)
FindFakerNodeFunc func(name string) *generator.Node
m sync.Mutex
StoreTest *engine.Store
@@ -120,9 +120,9 @@ func (h *Host) Cron(expr string, do func(), opt common.JobOptions) (int, error)
return 0, nil
}
-func (h *Host) On(event string, do common.EventHandler, tags map[string]string) {
+func (h *Host) On(event string, do common.EventHandler, args common.EventArgs) {
if h.OnFunc != nil {
- h.OnFunc(event, do, tags)
+ h.OnFunc(event, do, args)
}
}
diff --git a/engine/event.go b/engine/event.go
new file mode 100644
index 000000000..919221532
--- /dev/null
+++ b/engine/event.go
@@ -0,0 +1,54 @@
+package engine
+
+import (
+ "cmp"
+ "mokapi/engine/common"
+ "slices"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+func (e *Engine) Run(event string, args ...interface{}) []*common.Action {
+ var ehs []*eventHandler
+ for _, h := range e.scripts {
+ ehs = append(ehs, h.events[event]...)
+ }
+ slices.SortStableFunc(ehs, func(a, b *eventHandler) int { return -1 * cmp.Compare(a.priority, b.priority) })
+
+ var result []*common.Action
+
+ for _, eh := range ehs {
+ a := runEventHandler(eh, args...)
+ if a != nil {
+ result = append(result, a)
+ }
+ }
+
+ return result
+}
+
+func runEventHandler(eh *eventHandler, args ...interface{}) *common.Action {
+ action := &common.Action{
+ Tags: eh.tags,
+ }
+ start := time.Now()
+ logs := len(action.Logs)
+
+ ctx := &common.EventContext{
+ EventLogger: action.AppendLog,
+ Args: args,
+ }
+
+ if b, err := eh.handler(ctx); err != nil {
+ log.Errorf("unable to execute event handler: %v", err)
+ action.Error = &common.Error{Message: err.Error()}
+ } else if !b && logs == len(action.Logs) {
+ return nil
+ }
+ log.WithField("handler", action).Debug("processed event handler")
+
+ action.Parameters = getDeepCopy(args)
+ action.Duration = time.Now().Sub(start).Milliseconds()
+ return action
+}
diff --git a/engine/host.go b/engine/host.go
index 546e79d82..3ccf51c7c 100644
--- a/engine/host.go
+++ b/engine/host.go
@@ -18,8 +18,9 @@ import (
)
type eventHandler struct {
- handler common.EventHandler
- tags map[string]string
+ handler common.EventHandler
+ tags map[string]string
+ priority int
}
type scriptHost struct {
@@ -70,36 +71,6 @@ func (sh *scriptHost) Run() (err error) {
return sh.script.Run()
}
-func (sh *scriptHost) RunEvent(event string, args ...interface{}) []*common.Action {
- var result []*common.Action
- for _, eh := range sh.events[event] {
- action := &common.Action{
- Tags: eh.tags,
- }
- start := time.Now()
- logs := len(action.Logs)
-
- ctx := &common.EventContext{
- EventLogger: action.AppendLog,
- Args: args,
- }
-
- if b, err := eh.handler(ctx); err != nil {
- log.Errorf("unable to execute event handler: %v", err)
- action.Error = &common.Error{Message: err.Error()}
- } else if !b && logs == len(action.Logs) {
- continue
- } else {
- log.WithField("handler", action).Debug("processed event handler")
- }
-
- action.Parameters = getDeepCopy(args)
- action.Duration = time.Now().Sub(start).Milliseconds()
- result = append(result, action)
- }
- return result
-}
-
func (sh *scriptHost) Every(every string, handler func(), opt common.JobOptions) (int, error) {
id := len(sh.jobs)
@@ -199,7 +170,7 @@ func (sh *scriptHost) Cancel(jobId int) error {
}
}
-func (sh *scriptHost) On(event string, handler common.EventHandler, tags map[string]string) {
+func (sh *scriptHost) On(event string, handler common.EventHandler, args common.EventArgs) {
h := &eventHandler{
handler: handler,
tags: map[string]string{
@@ -208,9 +179,10 @@ func (sh *scriptHost) On(event string, handler common.EventHandler, tags map[str
"fileKey": sh.file.Info.Key(),
"event": event,
},
+ priority: args.Priority,
}
- for k, v := range tags {
+ for k, v := range args.Tags {
h.tags[k] = v
}
diff --git a/engine/mokapi_on_test.go b/engine/mokapi_on_test.go
index 5b353f576..d3b56a73a 100644
--- a/engine/mokapi_on_test.go
+++ b/engine/mokapi_on_test.go
@@ -242,6 +242,44 @@ export default () => {
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
},
},
+ {
+ name: "order of event handler execution",
+ script: `import { on } from 'mokapi'
+export default () => {
+ let counter = 0
+ on('http', (req, res) => {
+ res.data.foo = 'handler1';
+ res.data.handler1 = counter++
+ }, { priority: -1 })
+ on('http', (req, res) => {
+ res.data.foo = 'handler2';
+ res.data.handler2 = counter++
+ }, { priority: 10 })
+ on('http', (req, res) => {
+ res.data.foo = 'handler3';
+ res.data.handler3 = counter++
+ })
+}
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.EventRequest{}, res)
+ require.Nil(t, actions[0].Error)
+ require.Equal(t, map[string]any{"foo": "handler1", "handler1": int64(2), "handler2": int64(0), "handler3": int64(1)}, mokapi.Export(res.Data))
+ return actions
+ },
+ test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
+ require.NoError(t, err)
+
+ var res *common.EventResponse
+ err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler2", "handler2": float64(0)}, mokapi.Export(res.Data))
+ err = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler3", "handler2": float64(0), "handler3": float64(1)}, mokapi.Export(res.Data))
+ err = json.Unmarshal([]byte(actions[2].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler1", "handler2": float64(0), "handler3": float64(1), "handler1": float64(2)}, mokapi.Export(res.Data))
+ },
+ },
}
for _, tc := range testcases {
diff --git a/js/mokapi/on.go b/js/mokapi/on.go
index 5e234a806..fb3c5f75c 100644
--- a/js/mokapi/on.go
+++ b/js/mokapi/on.go
@@ -17,6 +17,7 @@ type onArgs struct {
tags map[string]string
track bool
isTrackSet bool
+ priority int
}
func (m *Module) On(event string, do goja.Value, vArgs goja.Value) {
@@ -65,7 +66,7 @@ func (m *Module) On(event string, do goja.Value, vArgs goja.Value) {
return haveChanges(origin, newHashes), nil
}
- m.host.On(event, f, eventArgs.tags)
+ m.host.On(event, f, common.EventArgs{Tags: eventArgs.tags, Priority: eventArgs.priority})
}
func getOnArgs(vm *goja.Runtime, args goja.Value) (onArgs, error) {
@@ -100,6 +101,15 @@ func getOnArgs(vm *goja.Runtime, args goja.Value) (onArgs, error) {
}
result.track = v.ToBoolean()
result.isTrackSet = true
+ case "priority":
+ v := params.Get(k)
+ if goja.IsUndefined(v) || goja.IsNull(v) {
+ continue
+ }
+ if v.ExportType().Kind() != reflect.Int64 {
+ return onArgs{}, fmt.Errorf("unexpected type for priority: %v", util.JsType(v.Export()))
+ }
+ result.priority = int(v.ToInteger())
}
}
return result, nil
@@ -143,6 +153,11 @@ func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
p.KeyNormalizer = http.CanonicalHeaderKey
}
+ switch val.(type) {
+ case string, int, bool:
+ return p.vm.ToValue(val)
+ }
+
return vm.NewDynamicObject(p)
},
})
diff --git a/js/mokapi/on_test.go b/js/mokapi/on_test.go
index d9f0a9c5e..5a612e793 100644
--- a/js/mokapi/on_test.go
+++ b/js/mokapi/on_test.go
@@ -26,7 +26,7 @@ func TestModule_On(t *testing.T) {
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var event string
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
event = evt
handler = do
}
@@ -49,7 +49,7 @@ func TestModule_On(t *testing.T) {
name: "event handler with parameter",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -70,7 +70,7 @@ func TestModule_On(t *testing.T) {
name: "event handler changes params",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -88,7 +88,7 @@ func TestModule_On(t *testing.T) {
name: "event handler does not change params",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -106,7 +106,7 @@ func TestModule_On(t *testing.T) {
name: "event handler does not change params but uses track argument",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -124,7 +124,7 @@ func TestModule_On(t *testing.T) {
name: "event handler changes params but disables track",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -142,7 +142,7 @@ func TestModule_On(t *testing.T) {
name: "event handler throws error",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -159,8 +159,8 @@ func TestModule_On(t *testing.T) {
name: "event handler with tags",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var tags map[string]string
- host.OnFunc = func(evt string, do common.EventHandler, t map[string]string) {
- tags = t
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
+ tags = args.Tags
}
_, err := vm.RunString(`
@@ -191,11 +191,37 @@ func TestModule_On(t *testing.T) {
r.EqualError(t, err, "unexpected type for args: String at mokapi/js/mokapi.(*Module).On-fm (native)")
},
},
+ {
+ name: "event handler with priority",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ priority := 0
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
+ priority = args.Priority
+ }
+
+ _, err := vm.RunString(`
+ const m = require('mokapi')
+ m.on('http', () => true, { priority: 100 })
+ `)
+ r.NoError(t, err)
+ r.Equal(t, 100, priority)
+ },
+ },
+ {
+ name: "event handler invalid type for priority",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ _, err := vm.RunString(`
+ const m = require('mokapi')
+ m.on('http', () => true, { priority: 'foo' })
+ `)
+ r.EqualError(t, err, "unexpected type for priority: String at mokapi/js/mokapi.(*Module).On-fm (native)")
+ },
+ },
{
name: "async event handler",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
var handler common.EventHandler
- host.OnFunc = func(evt string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(evt string, do common.EventHandler, args common.EventArgs) {
handler = do
}
@@ -482,7 +508,7 @@ m.on('http', (req, res) => {
reg.Enable(vm)
var runEvent common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
runEvent = do
}
diff --git a/js/script.go b/js/script.go
index d008ea6c4..85ab6cd7e 100644
--- a/js/script.go
+++ b/js/script.go
@@ -213,7 +213,7 @@ func (s *Script) addHttpEvent(i interface{}) {
return engine.HttpEventHandler(req, res, i)
}
- s.host.On("http", f, nil)
+ s.host.On("http", f, engine.EventArgs{})
}
// customFieldNameMapper default implementation filters out
diff --git a/js/script_data_test.go b/js/script_data_test.go
index bef1f4927..018fe8c62 100644
--- a/js/script_data_test.go
+++ b/js/script_data_test.go
@@ -18,7 +18,7 @@ func TestScript_Data(t *testing.T) {
{
name: "resource array",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
request := &common.EventRequest{}
request.Url.Path = "/foo/bar"
@@ -39,7 +39,7 @@ export const mokapi = {http: {"bar": [1, 2, 3, 4]}}`),
{
name: "resource absolute precedence ",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
request := &common.EventRequest{}
request.Url.Path = "/foo/bar"
@@ -60,7 +60,7 @@ export const mokapi = {"http": {"bar": [5,6], "foo": {"bar": [1, 2, 3, 4]}}}`),
{
name: "using default function",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
request := &common.EventRequest{}
request.Url.Path = "/foo/bar"
diff --git a/js/script_mokapi_test.go b/js/script_mokapi_test.go
index 9ffc1fc4d..948e19de4 100644
--- a/js/script_mokapi_test.go
+++ b/js/script_mokapi_test.go
@@ -299,7 +299,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
{
name: "event",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
}
s, err := jstest.New(jstest.WithSource(
@@ -316,8 +316,8 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
{
name: "tags",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
- r.Equal(t, "bar", tags["foo"])
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
+ r.Equal(t, "bar", args.Tags["foo"])
}
s, err := jstest.New(jstest.WithSource(
`import { on } from 'mokapi'
@@ -348,7 +348,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
name: "run function",
test: func(t *testing.T, host *enginetest.Host) {
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -379,7 +379,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
name: "return value default is false",
test: func(t *testing.T, host *enginetest.Host) {
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -401,7 +401,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
name: "on error",
test: func(t *testing.T, host *enginetest.Host) {
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -429,7 +429,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
}
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -452,13 +452,13 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
name: "access kebab case property by bracket notation",
test: func(t *testing.T, host *enginetest.Host) {
data := &struct {
- Ship_date string `json:"ship-date"` // can be accessed via obj['ship-date'] in javascript
+ Ship_date string `json:"ship-date"` // can be accessed via obj['ship-date'] in JavaScript
}{
Ship_date: "2022-01-01",
}
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -483,7 +483,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
data := map[string]string{"foo": "bar"}
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -506,7 +506,7 @@ func TestScript_Mokapi_On_Http(t *testing.T) {
name: "logging and async",
test: func(t *testing.T, host *enginetest.Host) {
var doFunc common.EventHandler
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
doFunc = do
}
s, err := jstest.New(jstest.WithSource(
@@ -557,7 +557,7 @@ func TestScript_Mokapi_On_Kafka(t *testing.T) {
{
name: "event",
test: func(t *testing.T, host *enginetest.Host) {
- host.OnFunc = func(event string, do common.EventHandler, tags map[string]string) {
+ host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "kafka", event)
}
s, err := jstest.New(jstest.WithSource(
diff --git a/lua/modules/mokapi.go b/lua/modules/mokapi.go
index 6fb7e2d26..c851213ed 100644
--- a/lua/modules/mokapi.go
+++ b/lua/modules/mokapi.go
@@ -118,7 +118,7 @@ func (m *Mokapi) on(l *lua.LState) int {
}
}
- m.host.On(evt, fn, args.Tags)
+ m.host.On(evt, fn, common.EventArgs{Tags: args.Tags})
return 0
}
diff --git a/lua/modules/mokapi_test.go b/lua/modules/mokapi_test.go
index e44ed9cc1..e133a8f17 100644
--- a/lua/modules/mokapi_test.go
+++ b/lua/modules/mokapi_test.go
@@ -99,7 +99,7 @@ func TestMokapi_On(t *testing.T) {
t.Run("mokapi.on event", func(t *testing.T) {
var event string
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
event = evt
},
}
@@ -119,7 +119,7 @@ mokapi.on("foo", function() end)
t.Run("mokapi.on do returns true", func(t *testing.T) {
var fn common.EventHandler
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
fn = do
},
}
@@ -141,7 +141,7 @@ mokapi.on("foo", function() return true end)
t.Run("mokapi.on do got error", func(t *testing.T) {
var fn common.EventHandler
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
fn = do
},
}
@@ -166,7 +166,7 @@ end)
t.Run("mokapi.on with parameters", func(t *testing.T) {
var fn common.EventHandler
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
fn = do
},
}
@@ -192,8 +192,8 @@ end)
t.Run("mokapi.on tags", func(t *testing.T) {
var m map[string]string
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
- m = tags
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
+ m = args.Tags
},
}
l := lua.NewState(lua.Options{IncludeGoStackTrace: true})
@@ -212,7 +212,7 @@ mokapi.on("foo", function() return true end, {tags = {tag1 = "foo", tag2 = "bar"
t.Run("mokapi.on access variable", func(t *testing.T) {
var fn common.EventHandler
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
fn = do
},
}
@@ -239,7 +239,7 @@ return true end)
t.Run("mokapi.on two handlers", func(t *testing.T) {
var fns []common.EventHandler
host := &testHost{
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
fns = append(fns, do)
},
}
@@ -276,7 +276,7 @@ return true end)
type testHost struct {
common.Host
fnInfo func(s string)
- fnOn func(event string, do common.EventHandler, tags map[string]string)
+ fnOn func(event string, do common.EventHandler, args common.EventArgs)
fnEvery func(every string, do func(), opt common.JobOptions) (int, error)
}
@@ -286,9 +286,9 @@ func (th *testHost) Info(args ...interface{}) {
}
}
-func (th *testHost) On(event string, do common.EventHandler, tags map[string]string) {
+func (th *testHost) On(event string, do common.EventHandler, args common.EventArgs) {
if th.fnOn != nil {
- th.fnOn(event, do, tags)
+ th.fnOn(event, do, args)
}
}
diff --git a/lua/script_test.go b/lua/script_test.go
index ef32d15c2..727bdad83 100644
--- a/lua/script_test.go
+++ b/lua/script_test.go
@@ -53,7 +53,7 @@ func TestMokapi_On(t *testing.T) {
fnInfo: func(s string) {
log = s
},
- fnOn: func(evt string, do common.EventHandler, tags map[string]string) {
+ fnOn: func(evt string, do common.EventHandler, args common.EventArgs) {
called = true
_, err := do(&common.EventContext{})
require.NoError(t, err)
@@ -108,7 +108,7 @@ mustache.render("", {})
type testHost struct {
common.Host
fnInfo func(s string)
- fnOn func(event string, do common.EventHandler, tags map[string]string)
+ fnOn func(event string, do common.EventHandler, args common.EventArgs)
}
func (th *testHost) Info(args ...interface{}) {
@@ -117,9 +117,9 @@ func (th *testHost) Info(args ...interface{}) {
}
}
-func (th *testHost) On(event string, do common.EventHandler, tags map[string]string) {
+func (th *testHost) On(event string, do common.EventHandler, args common.EventArgs) {
if th.fnOn != nil {
- th.fnOn(event, do, tags)
+ th.fnOn(event, do, args)
}
}
From 7d012925d496591c14421942c2de24560ecf8d8c Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 23:18:02 +0100
Subject: [PATCH 57/75] test: fix forcing a write error
---
js/file/file_test.go | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/js/file/file_test.go b/js/file/file_test.go
index 5d76b5977..45e93a46a 100644
--- a/js/file/file_test.go
+++ b/js/file/file_test.go
@@ -139,9 +139,8 @@ func TestModule_Open(t *testing.T) {
host.CwdFunc = func() string {
return dir
}
- f, err := os.OpenFile(filepath.Join(dir, "foo.txt"), os.O_RDWR|os.O_CREATE, os.ModeExclusive)
+ err := os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("initial"), 0444)
r.NoError(t, err)
- defer f.Close()
_, err = vm.RunString(`
const m = require("mokapi/file")
From 4e379388c466235032abad64e98fc91dd629a91f Mon Sep 17 00:00:00 2001
From: marle3003
Date: Fri, 13 Feb 2026 23:25:56 +0100
Subject: [PATCH 58/75] test: fix forcing a write error
---
js/file/file_test.go | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/js/file/file_test.go b/js/file/file_test.go
index 45e93a46a..9be19d861 100644
--- a/js/file/file_test.go
+++ b/js/file/file_test.go
@@ -139,12 +139,10 @@ func TestModule_Open(t *testing.T) {
host.CwdFunc = func() string {
return dir
}
- err := os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("initial"), 0444)
- r.NoError(t, err)
- _, err = vm.RunString(`
+ _, err := vm.RunString(`
const m = require("mokapi/file")
- m.writeString('foo.txt', 'Hello World');
+ m.writeString('.', 'Hello World');
`)
r.ErrorContains(t, err, "failed to write to file: open")
},
From bfd0e139b637fa89b18ec74a2ab8dc9d2ea8b227 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 12:13:39 +0100
Subject: [PATCH 59/75] test(javascript): add test for event handlers priority
---
engine/mokapi_on_test.go | 114 ++++++++++++++++++++++++++++++++-------
1 file changed, 94 insertions(+), 20 deletions(-)
diff --git a/engine/mokapi_on_test.go b/engine/mokapi_on_test.go
index d3b56a73a..875dfb305 100644
--- a/engine/mokapi_on_test.go
+++ b/engine/mokapi_on_test.go
@@ -2,6 +2,7 @@ package engine_test
import (
"encoding/json"
+ "fmt"
"io"
"mokapi/engine"
"mokapi/engine/common"
@@ -242,9 +243,41 @@ export default () => {
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
},
},
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ logrus.SetOutput(io.Discard)
+ hook := test.NewGlobal()
+ logrus.SetLevel(logrus.InfoLevel)
+
+ var opts []engine.Options
+ if tc.logger != nil {
+ opts = append(opts, engine.WithLogger(tc.logger))
+ }
+
+ e := enginetest.NewEngine(opts...)
+ err := e.AddScript(newScript("test.js", tc.script))
+
+ var actions []*common.Action
+ if err == nil {
+ actions = tc.run(e)
+ }
+ tc.test(t, actions, hook, err)
+ })
+ }
+}
+
+func TestEventHandler_Priority(t *testing.T) {
+ testcases := []struct {
+ name string
+ scripts []string
+ run func(evt common.EventEmitter) []*common.Action
+ test func(t *testing.T, actions []*common.Action)
+ }{
{
- name: "order of event handler execution",
- script: `import { on } from 'mokapi'
+ name: "handlers in same script",
+ scripts: []string{`import { on } from 'mokapi'
export default () => {
let counter = 0
on('http', (req, res) => {
@@ -261,46 +294,87 @@ export default () => {
})
}
`,
+ },
run: func(evt common.EventEmitter) []*common.Action {
res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
actions := evt.Emit("http", &common.EventRequest{}, res)
require.Nil(t, actions[0].Error)
+ require.Nil(t, actions[1].Error)
+ require.Nil(t, actions[2].Error)
require.Equal(t, map[string]any{"foo": "handler1", "handler1": int64(2), "handler2": int64(0), "handler3": int64(1)}, mokapi.Export(res.Data))
return actions
},
- test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
- require.NoError(t, err)
-
+ test: func(t *testing.T, actions []*common.Action) {
var res *common.EventResponse
- err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ _ = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "handler2", "handler2": float64(0)}, mokapi.Export(res.Data))
- err = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
+ _ = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "handler3", "handler2": float64(0), "handler3": float64(1)}, mokapi.Export(res.Data))
- err = json.Unmarshal([]byte(actions[2].Parameters[1].(string)), &res)
+ _ = json.Unmarshal([]byte(actions[2].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "handler1", "handler2": float64(0), "handler3": float64(1), "handler1": float64(2)}, mokapi.Export(res.Data))
},
},
+ {
+ name: "handlers in different scripts",
+ scripts: []string{`
+import { on } from 'mokapi'
+export default () => {
+ on('http', (req, res) => {
+ res.data.foo = 'handler1';
+ }, { priority: -1 })
+}
+`,
+ `
+import { on } from 'mokapi'
+export default () => {
+ on('http', (req, res) => {
+ res.data.foo = 'handler2';
+ }, { priority: 10 })
+}
+`,
+ `
+import { on } from 'mokapi'
+export default () => {
+ on('http', (req, res) => {
+ res.data.foo = 'handler3';
+ })
+}
+`,
+ },
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.EventRequest{}, res)
+ require.Nil(t, actions[0].Error)
+ require.Nil(t, actions[1].Error)
+ require.Nil(t, actions[2].Error)
+ require.Equal(t, map[string]any{"foo": "handler1"}, mokapi.Export(res.Data))
+ return actions
+ },
+ test: func(t *testing.T, actions []*common.Action) {
+ var res *common.EventResponse
+ _ = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler2"}, mokapi.Export(res.Data))
+ _ = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler3"}, mokapi.Export(res.Data))
+ _ = json.Unmarshal([]byte(actions[2].Parameters[1].(string)), &res)
+ require.Equal(t, map[string]any{"foo": "handler1"}, mokapi.Export(res.Data))
+ },
+ },
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
logrus.SetOutput(io.Discard)
- hook := test.NewGlobal()
- logrus.SetLevel(logrus.InfoLevel)
var opts []engine.Options
- if tc.logger != nil {
- opts = append(opts, engine.WithLogger(tc.logger))
- }
-
e := enginetest.NewEngine(opts...)
- err := e.AddScript(newScript("test.js", tc.script))
-
- var actions []*common.Action
- if err == nil {
- actions = tc.run(e)
+ for i, s := range tc.scripts {
+ err := e.AddScript(newScript(fmt.Sprintf("test-%v.js", i), s))
+ require.NoError(t, err)
}
- tc.test(t, actions, hook, err)
+
+ actions := tc.run(e)
+ tc.test(t, actions)
})
}
}
From 7df9d09b8a3807cfe6b1224c93c334893a189db7 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 13:32:53 +0100
Subject: [PATCH 60/75] refactor: rename type
---
engine/common/http.go | 10 +--
engine/common/http_test.go | 8 +--
engine/mokapi_on_test.go | 62 +++++++++----------
js/mokapi/on.go | 2 +-
js/mokapi/on_test.go | 50 +++++++--------
js/script.go | 4 +-
js/script_data_test.go | 12 ++--
providers/openapi/event.go | 18 +++---
providers/openapi/handler_requestbody_test.go | 22 +++----
providers/openapi/handler_response_test.go | 20 +++---
providers/openapi/handler_security_test.go | 35 ++++++-----
providers/openapi/handler_test.go | 46 +++++++-------
12 files changed, 145 insertions(+), 144 deletions(-)
diff --git a/engine/common/http.go b/engine/common/http.go
index 46db0412c..3056a2063 100644
--- a/engine/common/http.go
+++ b/engine/common/http.go
@@ -6,14 +6,14 @@ import (
"strings"
)
-type EventResponse struct {
+type HttpEventResponse struct {
Headers map[string]any `json:"headers"`
StatusCode int `json:"statusCode"`
Body string `json:"body"`
Data any `json:"data"`
}
-type EventRequest struct {
+type HttpEventRequest struct {
Method string `json:"method"`
Url Url `json:"url"`
Body interface{} `json:"body"`
@@ -36,7 +36,7 @@ type Url struct {
Query string `json:"query"`
}
-func (r *EventRequest) String() string {
+func (r *HttpEventRequest) String() string {
s := r.Method + " " + r.Url.String()
if r.Api != "" {
s += fmt.Sprintf(" [API: %s]", r.Api)
@@ -61,11 +61,11 @@ func (u Url) String() string {
return sb.String()
}
-func (r *EventResponse) HasBody() bool {
+func (r *HttpEventResponse) HasBody() bool {
return len(r.Body) > 0 || r.Data != nil
}
-func HttpEventHandler(req *EventRequest, res *EventResponse, resources interface{}) (bool, error) {
+func HttpEventHandler(req *HttpEventRequest, res *HttpEventResponse, resources interface{}) (bool, error) {
resource := getResource(req.Url, resources)
if resource == nil {
return false, nil
diff --git a/engine/common/http_test.go b/engine/common/http_test.go
index cfebca231..89a1c5adf 100644
--- a/engine/common/http_test.go
+++ b/engine/common/http_test.go
@@ -7,7 +7,7 @@ import (
)
func TestEventRequest_String(t *testing.T) {
- r := &EventRequest{
+ r := &HttpEventRequest{
Method: "GET",
Url: Url{
Scheme: "https",
@@ -20,7 +20,7 @@ func TestEventRequest_String(t *testing.T) {
}
func TestEventResponse_HasBody(t *testing.T) {
- r := &EventResponse{}
+ r := &HttpEventResponse{}
require.False(t, r.HasBody())
r.Body = "foo"
require.True(t, r.HasBody())
@@ -55,10 +55,10 @@ func TestHttpResource(t *testing.T) {
tc := tc
t.Run(tc.url.String(), func(t *testing.T) {
t.Parallel()
- req := &EventRequest{
+ req := &HttpEventRequest{
Url: tc.url,
}
- res := &EventResponse{}
+ res := &HttpEventResponse{}
b, err := HttpEventHandler(req, res, tc.resource)
tc.test(t, b, res.Data, err)
})
diff --git a/engine/mokapi_on_test.go b/engine/mokapi_on_test.go
index 875dfb305..39b6b724b 100644
--- a/engine/mokapi_on_test.go
+++ b/engine/mokapi_on_test.go
@@ -33,10 +33,10 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{
+ res := &common.HttpEventResponse{
Headers: map[string]any{"Content-Type": "application/json"},
}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Equal(t, "text/plain", mokapi.Export(res.Headers["Content-Type"]))
return actions
},
@@ -44,7 +44,7 @@ export default () => {
require.NoError(t, err)
require.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Len(t, res.Headers, 1)
require.Equal(t, "text/plain", res.Headers["Content-Type"])
@@ -60,8 +60,8 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Equal(t, &map[string]interface{}{"foo": "bar"}, res.Data)
return actions
},
@@ -69,7 +69,7 @@ export default () => {
require.NoError(t, err)
require.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]interface{}{"foo": "bar"}, res.Data)
},
@@ -84,8 +84,8 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Equal(t, 201, res.StatusCode)
return actions
},
@@ -93,7 +93,7 @@ export default () => {
require.NoError(t, err)
require.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, 201, res.StatusCode)
},
@@ -108,14 +108,14 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- return evt.Emit("http", &common.EventRequest{}, &common.EventResponse{})
+ return evt.Emit("http", &common.HttpEventRequest{}, &common.HttpEventResponse{})
},
test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
require.NoError(t, err)
require.NotNil(t, actions[0].Error)
require.Equal(t, "failed to set statusCode: expected Integer but got String at test.js:4:6(3)", actions[0].Error.Message)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, 0, res.StatusCode)
require.Len(t, hook.Entries, 2)
@@ -132,8 +132,8 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Equal(t, "hello world", res.Body)
return actions
},
@@ -141,7 +141,7 @@ export default () => {
require.NoError(t, err)
require.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, "hello world", res.Body)
},
@@ -156,14 +156,14 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- return evt.Emit("http", &common.EventRequest{}, &common.EventResponse{})
+ return evt.Emit("http", &common.HttpEventRequest{}, &common.HttpEventResponse{})
},
test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
require.NoError(t, err)
require.NotNil(t, actions[0].Error)
require.Equal(t, "failed to set body: expected String but got Object at test.js:4:6(5)", actions[0].Error.Message)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, "", res.Body)
require.Len(t, hook.Entries, 2)
@@ -181,15 +181,15 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Equal(t, &[]any{int64(1), int64(2), int64(3)}, res.Data)
return actions
},
test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
require.NoError(t, err)
require.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, []any{float64(1), float64(2), float64(3)}, res.Data)
},
@@ -205,8 +205,8 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Nil(t, actions[0].Error)
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
return actions
@@ -214,7 +214,7 @@ export default () => {
test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
require.NoError(t, err)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
},
@@ -229,8 +229,8 @@ export default () => {
}
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Nil(t, actions[0].Error)
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
return actions
@@ -238,7 +238,7 @@ export default () => {
test: func(t *testing.T, actions []*common.Action, hook *test.Hook, err error) {
require.NoError(t, err)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
},
@@ -296,8 +296,8 @@ export default () => {
`,
},
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Nil(t, actions[0].Error)
require.Nil(t, actions[1].Error)
require.Nil(t, actions[2].Error)
@@ -305,7 +305,7 @@ export default () => {
return actions
},
test: func(t *testing.T, actions []*common.Action) {
- var res *common.EventResponse
+ var res *common.HttpEventResponse
_ = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "handler2", "handler2": float64(0)}, mokapi.Export(res.Data))
_ = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
@@ -342,8 +342,8 @@ export default () => {
`,
},
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
require.Nil(t, actions[0].Error)
require.Nil(t, actions[1].Error)
require.Nil(t, actions[2].Error)
@@ -351,7 +351,7 @@ export default () => {
return actions
},
test: func(t *testing.T, actions []*common.Action) {
- var res *common.EventResponse
+ var res *common.HttpEventResponse
_ = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
require.Equal(t, map[string]any{"foo": "handler2"}, mokapi.Export(res.Data))
_ = json.Unmarshal([]byte(actions[1].Parameters[1].(string)), &res)
diff --git a/js/mokapi/on.go b/js/mokapi/on.go
index fb3c5f75c..3242eddc1 100644
--- a/js/mokapi/on.go
+++ b/js/mokapi/on.go
@@ -141,7 +141,7 @@ func haveChanges(origin [][]byte, new [][]byte) bool {
func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
switch v := (arg).(type) {
- case *common.EventResponse:
+ case *common.HttpEventResponse:
return vm.NewDynamicObject(&Proxy{
target: reflect.ValueOf(v),
vm: vm,
diff --git a/js/mokapi/on_test.go b/js/mokapi/on_test.go
index 5a612e793..3208fa12c 100644
--- a/js/mokapi/on_test.go
+++ b/js/mokapi/on_test.go
@@ -291,10 +291,10 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{
+ res := &common.HttpEventResponse{
Headers: map[string]any{"Content-Type": "application/json"},
}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
ct := res.Headers["Content-Type"].(*string)
r.Equal(t, "text/plain", *ct)
return actions
@@ -303,7 +303,7 @@ m.on('http', (req, res) => {
r.NoError(t, err)
r.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Len(t, res.Headers, 1)
r.Equal(t, "text/plain", res.Headers["Content-Type"])
@@ -318,8 +318,8 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Equal(t, map[string]interface{}{"foo": "bar"}, mokapi.Export(res.Data))
return actions
},
@@ -327,7 +327,7 @@ m.on('http', (req, res) => {
r.NoError(t, err)
r.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, map[string]interface{}{"foo": "bar"}, res.Data)
},
@@ -341,8 +341,8 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Equal(t, 201, res.StatusCode)
return actions
},
@@ -350,7 +350,7 @@ m.on('http', (req, res) => {
r.NoError(t, err)
r.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, 201, res.StatusCode)
},
@@ -364,14 +364,14 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- return evt.Emit("http", &common.EventRequest{}, &common.EventResponse{})
+ return evt.Emit("http", &common.HttpEventRequest{}, &common.HttpEventResponse{})
},
test: func(t *testing.T, actions []*common.Action, err error) {
r.NoError(t, err)
r.NotNil(t, actions[0].Error)
r.Equal(t, "failed to set statusCode: expected Integer but got String at :4:6(3)", actions[0].Error.Message)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, 0, res.StatusCode)
},
@@ -385,8 +385,8 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Equal(t, "hello world", res.Body)
return actions
},
@@ -394,7 +394,7 @@ m.on('http', (req, res) => {
r.NoError(t, err)
r.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, "hello world", res.Body)
},
@@ -408,14 +408,14 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- return evt.Emit("http", &common.EventRequest{}, &common.EventResponse{})
+ return evt.Emit("http", &common.HttpEventRequest{}, &common.HttpEventResponse{})
},
test: func(t *testing.T, actions []*common.Action, err error) {
r.NoError(t, err)
r.NotNil(t, actions[0].Error)
r.Equal(t, "failed to set body: expected String but got Object at :4:6(5)", actions[0].Error.Message)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, "", res.Body)
},
@@ -430,15 +430,15 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Equal(t, &[]any{int64(1), int64(2), int64(3)}, res.Data)
return actions
},
test: func(t *testing.T, actions []*common.Action, err error) {
r.NoError(t, err)
r.Nil(t, actions[0].Error)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, []any{float64(1), float64(2), float64(3)}, res.Data)
},
@@ -453,8 +453,8 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Nil(t, actions[0].Error)
r.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
return actions
@@ -462,7 +462,7 @@ m.on('http', (req, res) => {
test: func(t *testing.T, actions []*common.Action, err error) {
r.NoError(t, err)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
},
@@ -476,8 +476,8 @@ m.on('http', (req, res) => {
})
`,
run: func(evt common.EventEmitter) []*common.Action {
- res := &common.EventResponse{Data: map[string]any{"foo": "bar"}}
- actions := evt.Emit("http", &common.EventRequest{}, res)
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ actions := evt.Emit("http", &common.HttpEventRequest{}, res)
r.Nil(t, actions[0].Error)
r.Equal(t, map[string]any{"foo": "yuh"}, mokapi.Export(res.Data))
return actions
@@ -485,7 +485,7 @@ m.on('http', (req, res) => {
test: func(t *testing.T, actions []*common.Action, err error) {
r.NoError(t, err)
- var res *common.EventResponse
+ var res *common.HttpEventResponse
err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
r.Equal(t, map[string]any{"foo": "yuh"}, res.Data)
},
diff --git a/js/script.go b/js/script.go
index 85ab6cd7e..ab02626ce 100644
--- a/js/script.go
+++ b/js/script.go
@@ -208,8 +208,8 @@ func (s *Script) addHttpEvent(i interface{}) {
if len(ctx.Args) != 2 {
return false, fmt.Errorf("expected args: request, response")
}
- req := ctx.Args[0].(*engine.EventRequest)
- res := ctx.Args[1].(*engine.EventResponse)
+ req := ctx.Args[0].(*engine.HttpEventRequest)
+ res := ctx.Args[1].(*engine.HttpEventResponse)
return engine.HttpEventHandler(req, res, i)
}
diff --git a/js/script_data_test.go b/js/script_data_test.go
index 018fe8c62..3b8d19071 100644
--- a/js/script_data_test.go
+++ b/js/script_data_test.go
@@ -20,9 +20,9 @@ func TestScript_Data(t *testing.T) {
test: func(t *testing.T, host *enginetest.Host) {
host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
- request := &common.EventRequest{}
+ request := &common.HttpEventRequest{}
request.Url.Path = "/foo/bar"
- response := &common.EventResponse{}
+ response := &common.HttpEventResponse{}
b, err := do(&common.EventContext{Args: []any{request, response}})
r.NoError(t, err)
r.True(t, b)
@@ -41,9 +41,9 @@ export const mokapi = {http: {"bar": [1, 2, 3, 4]}}`),
test: func(t *testing.T, host *enginetest.Host) {
host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
- request := &common.EventRequest{}
+ request := &common.HttpEventRequest{}
request.Url.Path = "/foo/bar"
- response := &common.EventResponse{}
+ response := &common.HttpEventResponse{}
b, err := do(&common.EventContext{Args: []any{request, response}})
r.NoError(t, err)
r.True(t, b)
@@ -62,9 +62,9 @@ export const mokapi = {"http": {"bar": [5,6], "foo": {"bar": [1, 2, 3, 4]}}}`),
test: func(t *testing.T, host *enginetest.Host) {
host.OnFunc = func(event string, do common.EventHandler, args common.EventArgs) {
r.Equal(t, "http", event)
- request := &common.EventRequest{}
+ request := &common.HttpEventRequest{}
request.Url.Path = "/foo/bar"
- response := &common.EventResponse{}
+ response := &common.HttpEventResponse{}
b, err := do(&common.EventContext{Args: []any{request, response}})
r.NoError(t, err)
r.True(t, b)
diff --git a/providers/openapi/event.go b/providers/openapi/event.go
index cd638e9af..d58392c75 100644
--- a/providers/openapi/event.go
+++ b/providers/openapi/event.go
@@ -18,8 +18,8 @@ import (
const eventKey = "event"
-func NewEventResponse(status int, ct media.ContentType) *common.EventResponse {
- r := &common.EventResponse{
+func NewEventResponse(status int, ct media.ContentType) *common.HttpEventResponse {
+ r := &common.HttpEventResponse{
Headers: make(map[string]any),
StatusCode: status,
}
@@ -31,17 +31,17 @@ func NewEventResponse(status int, ct media.ContentType) *common.EventResponse {
return r
}
-func EventRequestFromContext(ctx context.Context) *common.EventRequest {
- e := ctx.Value(eventKey).(*common.EventRequest)
+func EventRequestFromContext(ctx context.Context) *common.HttpEventRequest {
+ e := ctx.Value(eventKey).(*common.HttpEventRequest)
return e
}
-func NewEventRequest(r *http.Request, contentType media.ContentType, api string) (*common.EventRequest, context.Context) {
+func NewEventRequest(r *http.Request, contentType media.ContentType, api string) (*common.HttpEventRequest, context.Context) {
ctx := r.Context()
endpointPath := ctx.Value("endpointPath").(string)
op, _ := OperationFromContext(ctx)
- req := &common.EventRequest{
+ req := &common.HttpEventRequest{
Api: api,
Key: endpointPath,
OperationId: op.OperationId,
@@ -88,7 +88,7 @@ func NewEventRequest(r *http.Request, contentType media.ContentType, api string)
return req, context.WithValue(ctx, eventKey, req)
}
-func setResponseData(r *common.EventResponse, m *MediaType, request *common.EventRequest) error {
+func setResponseData(r *common.HttpEventResponse, m *MediaType, request *common.HttpEventRequest) error {
if m != nil {
if len(m.Examples) > 0 {
keys := reflect.ValueOf(m.Examples).MapKeys()
@@ -125,7 +125,7 @@ func setResponseData(r *common.EventResponse, m *MediaType, request *common.Even
return nil
}
-func setResponseHeader(r *common.EventResponse, headers Headers) error {
+func setResponseHeader(r *common.HttpEventResponse, headers Headers) error {
for k, v := range headers {
if v.Value == nil {
log.Warnf("header ref not resovled: %v", v.Ref)
@@ -140,7 +140,7 @@ func setResponseHeader(r *common.EventResponse, headers Headers) error {
return nil
}
-func getGeneratorContext(r *common.EventRequest) map[string]interface{} {
+func getGeneratorContext(r *common.HttpEventRequest) map[string]interface{} {
ctx := map[string]interface{}{}
for k, v := range r.Cookie {
if v != nil {
diff --git a/providers/openapi/handler_requestbody_test.go b/providers/openapi/handler_requestbody_test.go
index d4035d291..fab206c00 100644
--- a/providers/openapi/handler_requestbody_test.go
+++ b/providers/openapi/handler_requestbody_test.go
@@ -3,7 +3,7 @@ package openapi_test
import (
"bytes"
"io"
- engine2 "mokapi/engine/common"
+ "mokapi/engine/common"
"mokapi/engine/enginetest"
"mokapi/providers/openapi"
"mokapi/providers/openapi/openapitest"
@@ -24,7 +24,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
name string
config *openapi.Config
fn func(t *testing.T, handler openapi.Handler)
- check func(t *testing.T, r *engine2.EventRequest)
+ check func(t *testing.T, r *common.HttpEventRequest)
}{
{
name: "text/plain",
@@ -50,7 +50,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, 200, rr.Code)
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
require.Equal(t, "foo", r.Body)
},
},
@@ -78,7 +78,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, 200, rr.Code)
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
require.Equal(t, "foo", r.Body)
},
},
@@ -108,7 +108,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, 200, rr.Code)
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
require.Equal(t, "foo", r.Body)
},
},
@@ -138,7 +138,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, 200, rr.Code)
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
require.Equal(t, map[string]interface{}{"bar": float64(12), "foo": "abc"}, r.Body)
},
},
@@ -168,7 +168,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, http.StatusOK, rr.Code)
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
},
},
{
@@ -195,7 +195,7 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
require.Equal(t, http.StatusOK, rr.Code)
require.True(t, spy.readCalled, "server needs to read body")
},
- check: func(t *testing.T, r *engine2.EventRequest) {
+ check: func(t *testing.T, r *common.HttpEventRequest) {
},
},
}
@@ -205,9 +205,9 @@ func TestResponseHandler_ServeHTTP_ResponseBody(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
test.NewNullLogger()
- var r *engine2.EventRequest
- e := enginetest.NewEngineWithHandler(func(event string, args ...interface{}) []*engine2.Action {
- r = args[0].(*engine2.EventRequest)
+ var r *common.HttpEventRequest
+ e := enginetest.NewEngineWithHandler(func(event string, args ...interface{}) []*common.Action {
+ r = args[0].(*common.HttpEventRequest)
return nil
})
diff --git a/providers/openapi/handler_response_test.go b/providers/openapi/handler_response_test.go
index eb0ecd7b5..2a7ea6e6f 100644
--- a/providers/openapi/handler_response_test.go
+++ b/providers/openapi/handler_response_test.go
@@ -36,14 +36,14 @@ func TestHandler_Response(t *testing.T) {
testcases := []struct {
name string
config *openapi.Config
- handler func(event string, req *common.EventRequest, res *common.EventResponse)
+ handler func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse)
req func() *http.Request
test func(t *testing.T, rr *httptest.ResponseRecorder, eh events.Handler)
}{
{
name: "string as response body",
config: getConfig(schematest.New("string"), "application/json"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Body = "foo"
},
req: func() *http.Request {
@@ -57,7 +57,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "invalid string body",
config: getConfig(schematest.New("string", schematest.WithFormat("date")), "application/json"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = "foo"
},
req: func() *http.Request {
@@ -71,7 +71,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "object with null property",
config: getConfig(schematest.New("object", schematest.WithProperty("foo", schematest.New("string", schematest.IsNullable(true)))), "application/json"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = map[string]interface{}{"foo": nil}
},
req: func() *http.Request {
@@ -85,7 +85,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "detect content type on byte array",
config: getConfig(schematest.New("object", schematest.WithProperty("foo", schematest.New("string"))), "*/*"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = []byte(`{"foo":"bar"}`)
},
req: func() *http.Request {
@@ -100,7 +100,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "application/octet-stream with string",
config: getConfig(schematest.New("string"), "application/octet-stream"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = "foo"
},
req: func() *http.Request {
@@ -114,7 +114,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "application/octet-stream with object",
config: getConfig(schematest.New("object"), "application/octet-stream"),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = map[string]interface{}{"foo": "bar"}
},
req: func() *http.Request {
@@ -128,7 +128,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "no content defined should send empty response body",
config: getConfig(nil, ""),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Data = map[string]interface{}{"foo": "bar"}
},
req: func() *http.Request {
@@ -149,7 +149,7 @@ func TestHandler_Response(t *testing.T) {
{
name: "no content defined should send body, when res.Body is used",
config: getConfig(nil, ""),
- handler: func(event string, req *common.EventRequest, res *common.EventResponse) {
+ handler: func(event string, req *common.HttpEventRequest, res *common.HttpEventResponse) {
res.Headers["Content-Type"] = "text/plain"
res.Body = "foo"
},
@@ -178,7 +178,7 @@ func TestHandler_Response(t *testing.T) {
m.SetStore(10, events.NewTraits().WithNamespace("http"))
e := &engine{emit: func(event string, args ...interface{}) []*common.Action {
- tc.handler(event, args[0].(*common.EventRequest), args[1].(*common.EventResponse))
+ tc.handler(event, args[0].(*common.HttpEventRequest), args[1].(*common.HttpEventResponse))
return []*common.Action{
{
Duration: 16,
diff --git a/providers/openapi/handler_security_test.go b/providers/openapi/handler_security_test.go
index 5670e213c..5133a61f9 100644
--- a/providers/openapi/handler_security_test.go
+++ b/providers/openapi/handler_security_test.go
@@ -2,7 +2,6 @@ package openapi_test
import (
"fmt"
- "github.com/stretchr/testify/require"
"mokapi/engine/common"
"mokapi/providers/openapi"
"mokapi/providers/openapi/openapitest"
@@ -10,6 +9,8 @@ import (
"net/http"
"net/http/httptest"
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestHandler_Security(t *testing.T) {
@@ -44,8 +45,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "Basic 123", *httpLog.Request.Parameters[0].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Header["Authorization"]
return nil
},
@@ -102,8 +103,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "Bearer 123", *httpLog.Request.Parameters[0].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Header["Authorization"]
return nil
},
@@ -136,8 +137,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "123", *httpLog.Request.Parameters[0].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Header["X-API-KEY"]
return nil
},
@@ -170,8 +171,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "123", *httpLog.Request.Parameters[0].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Query["apikey"]
return nil
},
@@ -204,8 +205,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "123", *httpLog.Request.Parameters[1].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Cookie["apikey"]
return nil
},
@@ -256,8 +257,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, "Bearer 123", *httpLog.Request.Parameters[0].Raw)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Header["Authorization"]
return nil
},
@@ -293,8 +294,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, `"Bearer 123 - API_KEY_123"`, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = fmt.Sprintf("%s - %s", req.Header["Authorization"], req.Header["apikey"])
return nil
},
@@ -325,8 +326,8 @@ func TestHandler_Security(t *testing.T) {
require.Equal(t, `"API_KEY_123"`, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- r := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ r := args[1].(*common.HttpEventResponse)
r.Data = req.Header["apikey"]
return nil
},
diff --git a/providers/openapi/handler_test.go b/providers/openapi/handler_test.go
index 52b82acf9..1f48b7adb 100644
--- a/providers/openapi/handler_test.go
+++ b/providers/openapi/handler_test.go
@@ -785,7 +785,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "no configuration was found for HTTP status code 415, https://swagger.io/docs/specification/describing-responses\n", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- r := args[1].(*common.EventResponse)
+ r := args[1].(*common.HttpEventResponse)
r.StatusCode = http.StatusUnsupportedMediaType
return nil
},
@@ -804,7 +804,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "no configuration was found for HTTP status code 415, https://swagger.io/docs/specification/describing-responses\n", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- r := args[1].(*common.EventResponse)
+ r := args[1].(*common.HttpEventResponse)
r.StatusCode = http.StatusUnsupportedMediaType
return nil
},
@@ -825,7 +825,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "text/plain", rr.Header().Get("Content-Type"))
},
event: func(event string, args ...interface{}) []*common.Action {
- r := args[1].(*common.EventResponse)
+ r := args[1].(*common.HttpEventResponse)
r.Headers["Content-Type"] = "text/plain"
r.Body = "Hello"
return nil
@@ -849,8 +849,8 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, `{"foo":"bar"}`, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- res := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ res := args[1].(*common.HttpEventResponse)
res.Data = req.Body
return nil
},
@@ -871,8 +871,8 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- res := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ res := args[1].(*common.HttpEventResponse)
res.Data = req.Body
return nil
},
@@ -896,8 +896,8 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, `"123"`, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- res := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ res := args[1].(*common.HttpEventResponse)
res.Data = req.Path["id"]
return nil
},
@@ -921,8 +921,8 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, `"123"`, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- res := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ res := args[1].(*common.HttpEventResponse)
res.Data = req.Path["id"]
return nil
},
@@ -946,7 +946,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, http.Header{"Content-Type": []string{"application/json"}, "Foo": []string{"12345"}}, rr.Header())
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["foo"] = "12345"
return nil
},
@@ -976,7 +976,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, map[string]string{"Content-Type": "application/json", "Foobaryuh": "12345"}, log.Response.Headers)
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["FooBarYuh"] = "12345"
return nil
},
@@ -998,11 +998,11 @@ func TestHandler_Event(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "http://localhost/foo/123", nil)
rr := httptest.NewRecorder()
h(rr, r)
- var er *common.EventRequest
+ var er *common.HttpEventRequest
b := rr.Body.Bytes()
err := json.Unmarshal(b, &er)
require.NoError(t, err)
- require.Equal(t, &common.EventRequest{
+ require.Equal(t, &common.HttpEventRequest{
Method: http.MethodGet,
Url: common.Url{
Scheme: "http",
@@ -1023,8 +1023,8 @@ func TestHandler_Event(t *testing.T) {
}, er)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
- res := args[1].(*common.EventResponse)
+ req := args[0].(*common.HttpEventRequest)
+ res := args[1].(*common.HttpEventResponse)
res.Data = req
return nil
},
@@ -1047,7 +1047,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["Content-Type"] = "text/plain"
res.Body = "hello world"
return nil
@@ -1071,7 +1071,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "response has no definition for content type: text/plain\n", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["Content-Type"] = "text/plain"
return nil
},
@@ -1094,7 +1094,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "invalid header 'Content-Type': expected a string or array of strings, but received Integer\n", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["Content-Type"] = 123
return nil
},
@@ -1118,7 +1118,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, []string{"1", "2"}, rr.Header()["Foo"])
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["foo"] = []any{"1", "2"}
return nil
},
@@ -1142,7 +1142,7 @@ func TestHandler_Event(t *testing.T) {
require.Equal(t, "invalid header 'foo': error count 1:\n\t- expected array but got: bar\n", rr.Body.String())
},
event: func(event string, args ...interface{}) []*common.Action {
- res := args[1].(*common.EventResponse)
+ res := args[1].(*common.HttpEventResponse)
res.Headers["foo"] = "bar"
return nil
},
@@ -1330,7 +1330,7 @@ func TestHandler_Parameter(t *testing.T) {
require.Equal(t, "missing parameter definition for route /foo/{id}: invalid path parameter 'id'", hook.Entries[0].Message)
},
event: func(event string, args ...interface{}) []*common.Action {
- req := args[0].(*common.EventRequest)
+ req := args[0].(*common.HttpEventRequest)
require.NotContains(t, req.Path, "id")
require.Len(t, req.Path, 0)
return nil
From 7e6863234e7fb27d63ac04d3b0da7ed732c7142b Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 13:34:06 +0100
Subject: [PATCH 61/75] doc: update documentations
---
docs/http/overview.md | 46 ++++++-------
.../mokapi/eventhandler/eventargs.md | 50 +++++++++++---
.../mokapi/eventhandler/eventhandler.md | 18 +++--
.../mokapi/eventhandler/httpeventhandler.md | 54 ++++++++++++---
.../mokapi/eventhandler/httprequest.md | 56 +++++++++++-----
.../mokapi/eventhandler/httpresponse.md | 67 ++++++++++++++++---
npm/types/index.d.ts | 2 +-
7 files changed, 221 insertions(+), 72 deletions(-)
diff --git a/docs/http/overview.md b/docs/http/overview.md
index 38163e223..4dba64365 100644
--- a/docs/http/overview.md
+++ b/docs/http/overview.md
@@ -321,11 +321,30 @@ Mokapi handles this translation automatically - you don't need to modify your sp
✅ Reference resolution handled automatically - Mokapi translates between formats
✅ Schema paths differ - Mokapi transforms the path
-## Best Practices
+## Best Practice: Adjust Only What Matters
-### Start with Auto-Generated Data
+Mokapi automatically generates a valid HTTP response based on the OpenAPI
+specification, including the status code, headers, and response body.
-Let Mokapi handle the basics. Adjust only data for you specific need.
+Event handlers are intended to modify **only the parts of the response that are
+relevant to a specific scenario**.
+
+Instead of constructing the entire response manually, developers can:
+- Override selected fields in `response.data`
+- Adjust headers as needed
+- Explicitly set a different status code (if defined in the OpenAPI specification)
+
+All other parts of the response remain automatically generated and valid.
+
+This approach:
+- Reduces boilerplate code
+- Prevents accidental invalid responses
+- Keeps event handlers focused and maintainable
+- Improves stability when APIs evolve: as response schemas change, event handlers
+ that modify only specific fields continue to work without requiring updates
+ for unrelated fields.
+
+### Example: Modifying a generated response
```javascript
import { on, sleep } from 'mokapi'
@@ -333,28 +352,9 @@ import { on, sleep } from 'mokapi'
export default function() {
on('http', (request, response) => {
if (request.key === '/pet/{petId}' && request.path.petId === 10) {
- // Instead of replacing entire responses, you can modify specific fields:
+ // Modify only the relevant part of the generated response
response.data.name = 'Garfield'
}
})
}
```
-
-### Version Your Specifications
-
-Keep your OpenAPI specs and Mokapi Scripts in version control alongside your code.
-
-### Use Scripts for Edge Cases
-
-Focus your scripts on:
-- Error scenarios (404, 500, validation errors)
-- Authentication/authorization testing
-- Specific business logic you want to test
-- Stateful workflows
-
-## Next Steps
-
-**Ready for more advanced features?**
-
-- [Test Data Generation](/docs/get-started/test-data.md) - Create realistic, varied test data
-- [Mokapi Scripts Guide](/docs/javascript-api/overview) - Full scripting reference and examples
diff --git a/docs/javascript-api/mokapi/eventhandler/eventargs.md b/docs/javascript-api/mokapi/eventhandler/eventargs.md
index d7f34d4b8..447b6c321 100644
--- a/docs/javascript-api/mokapi/eventhandler/eventargs.md
+++ b/docs/javascript-api/mokapi/eventhandler/eventargs.md
@@ -1,24 +1,58 @@
---
title: EventArgs
-description: EventArgs is an object used by on function.
+description: EventArgs is an object used to configure event handlers registered with the on function.
---
# EventArgs
-EventArgs is an object used by [on](/docs/javascript-api/mokapi/on.md) function.
+`EventArgs` is an optional configuration object passed to the
+[`on`](/docs/javascript-api/mokapi/on.md) function when registering an event handler.
+It allows controlling how and when an event handler is executed.
-| Name | Type | Description |
-|-------------------------|---------|--------------------------------------------------------------|
-| tags | object | Adds or overrides existing tags used in dashboard |
+| Name | Type | Description |
+|----------|---------|--------------------------------------------------------------------------------------------------------|
+| tags | object | Adds or overrides existing tags that are used in dashboard |
+| priority | integer | Defines the execution priority of the event handler. Handlers with a higher value are executed first. |
-## Examples
+If no priority is specified, the default priority is `0`.
-Add additional tag
+## Example: Adding custom tags
+
+The following example registers an event handler and adds a custom tag.
```javascript
import { every } from 'mokapi'
export default function() {
on('1m', function(request, response) {
+ // handler logic
}, { tags: { foo: 'bar' } })
}
-```
\ No newline at end of file
+```
+
+## Example: Controlling execution order with priority
+
+When multiple handlers are registered for the same event, the priority
+property controls the order in which they are executed.
+
+```javascript
+import { on } from 'mokapi'
+
+export default function() {
+ on('http', (req, res) => {
+ res.data.stage = 'early'
+ }, { priority: 10 })
+
+ on('http', (req, res) => {
+ res.data.stage = 'default'
+ })
+
+ on('http', (req, res) => {
+ res.data.stage = 'late'
+ }, { priority: -10 })
+}
+```
+
+In this example:
+- The handler with priority 10 is executed first
+- The handler without a priority is executed next (priority 0)
+- The handler with priority -10 is executed last
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi/eventhandler/eventhandler.md b/docs/javascript-api/mokapi/eventhandler/eventhandler.md
index 5a25fc2f3..da4901df7 100644
--- a/docs/javascript-api/mokapi/eventhandler/eventhandler.md
+++ b/docs/javascript-api/mokapi/eventhandler/eventhandler.md
@@ -4,13 +4,21 @@ description: EventHandler is a function that is executed when an event is trigge
---
# EventHandler
-EventHandler is a function that is executed when an event is triggered. EventHandler has event-specific parameters like HttpRequest that contains data about an HTTP request.
+An `EventHandler` is a function that is executed whenever a registered event is triggered.
+The parameters passed to the handler depend on the event type (for example, an HTTP event
+provides an `HttpRequest` and `HttpResponse` object).
-## Returns
+Multiple handlers can be registered for the same event.
-| Type | Description |
-|---------|-------------------------------------------------------|
-| boolean | Whether Mokapi should log execution of event handler. |
+## Usage
+
+Event handlers are registered using the `on` function.
+The first argument specifies the event type, the second argument is the handler function.
+
+## Example: Handling an HTTP event
+
+The following example registers an HTTP event handler that responds only to a specific
+operation (operationId === 'time').
```javascript
import { on } from 'mokapi'
diff --git a/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md b/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md
index e574ccad0..77100fbeb 100644
--- a/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md
+++ b/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md
@@ -1,23 +1,25 @@
---
title: HttpEventHandler
-description: HttpEventHandler is a function that is executed when an event is triggered.
+description: HttpEventHandler is a function that is executed when an HTTP event is triggered.
---
# HttpEventHandler
-HttpEventHandler is a function that is executed when an HTTP event is triggered.
+An `HttpEventHandler` is a function that is executed whenever an HTTP event is triggered.
+It allows inspecting the incoming request and modifying the outgoing response.
+
+Multiple HTTP event handlers can be registered.
+If more than one handler is registered, they are executed in order based on their `priority`.
| Parameter | Type | Description |
|-----------|--------|---------------------------------------------------------------------------------------------------------------------|
| request | object | [HttpRequest](/docs/javascript-api/mokapi/eventhandler/httprequest.md) object contains data of a HTTP request |
-| response | object | [HttpResponse](/docs/javascript-api/mokapi/eventhandler/httpresponse.md) object contains data for the HTTP response |
+| response | object | [HttpResponse](/docs/javascript-api/mokapi/eventhandler/httpresponse.md) object used to construct the HTTP response |
-## Returns
-| Type | Description |
-|---------|-------------------------------------------------------|
-| boolean | Whether Mokapi should log execution of event handler. |
+## Example: Handling a specific operation
-## Example
+The following example registers a handler for HTTP events and returns the current date
+when the request’s `operationId` is `time`.
```javascript
import { on, date } from 'mokapi'
@@ -26,9 +28,41 @@ export default function() {
on('http', function(request, response) {
if (request.operationId === 'time') {
response.body = date()
- return true
}
- return false
})
}
+```
+
+## Example: Controlling execution order with priority
+
+Multiple HTTP event handlers can be registered for the same event.
+The order in which they are executed is controlled by the priority option.
+
+Handlers with a higher priority value are executed first.
+If no priority is specified, the default priority is 0.
+
+```javascript
+import { on } from 'mokapi'
+
+export default () => {
+ let counter = 0
+
+ // Executed last (priority: -1)
+ on('http', (req, res) => {
+ res.data.foo = 'handler1';
+ res.data.handler1 = counter++
+ }, { priority: -1 })
+
+ // Executed first (priority: 10)
+ on('http', (req, res) => {
+ res.data.foo = 'handler2';
+ res.data.handler2 = counter++
+ }, { priority: 10 })
+
+ // Executed second (priority: 0)
+ on('http', (req, res) => {
+ res.data.foo = 'handler3';
+ res.data.handler3 = counter++
+ })
+}
```
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi/eventhandler/httprequest.md b/docs/javascript-api/mokapi/eventhandler/httprequest.md
index c754f43cd..c09ecfdc8 100644
--- a/docs/javascript-api/mokapi/eventhandler/httprequest.md
+++ b/docs/javascript-api/mokapi/eventhandler/httprequest.md
@@ -1,22 +1,46 @@
---
title: HttpRequest
-description: HttpRequest is an object used by HttpEventHandler
+description: HttpRequest is an object that provides access to request-specific data in an HTTP event handler.
---
# HttpRequest
-HttpRequest is an object used by [HttpEventHandler](/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md)
-that contains request-specific data such as HTTP headers.
-
-| Name | Type | Description |
-|-------------|--------|--------------------------------------------------------------------------|
-| method | string | Request method like `GET` |
-| url | object | Url represents a parsed URL |
-| key | string | Path value specified by the OpenAPI path |
-| operationId | string | OperationId defined in OpenAPI |
-| path | object | Object contains path parameters specified by OpenAPI path parameters |
-| query | object | Object contains query parameters specified by OpenAPI query parameters |
-| header | object | Object contains header parameters specified by OpenAPI header parameters |
-| cookie | object | Object contains cookie parameters specified by OpenAPI cookie parameters |
-| body | any | Body contains request body specified by OpenAPI request body |
-| api | string | The name of the API, as defined in the OpenAPI info.title field |
+`HttpRequest` is an object passed to an
+[`HttpEventHandler`](/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md).
+It contains request-specific data extracted from the incoming HTTP request
+and parsed according to the OpenAPI specification.
+The available properties depend on the OpenAPI definition and the incoming request.
+
+| Name | Type | Description |
+|-------------|--------|------------------------------------------------------------------|
+| method | string | HTTP request method, such as `GET` or `POST` |
+| url | object | Parsed URL of the request |
+| key | string | Path value that matched the OpenAPI path template |
+| operationId | string | `operationId` defined in the OpenAPI specification |
+| path | object | Path parameters defined by the OpenAPI path parameters |
+| query | object | Query parameters defined by the OpenAPI query parameters |
+| header | object | Header parameters defined by the OpenAPI header parameters |
+| cookie | object | Cookie parameters defined by the OpenAPI cookie parameters |
+| body | any | Request body parsed according to the OpenAPI request body schema |
+| api | string | Name of the API, as defined in the OpenAPI `info.title` field |
+
+## Example
+
+The following example demonstrates how to access request data inside an HTTP event handler.
+
+```javascript
+import { on } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ if (request.method === 'GET' && request.operationId === 'getUser') {
+ const userId = request.path.id
+ const includeDetails = request.query.details
+
+ response.body = {
+ id: userId,
+ details: includeDetails
+ }
+ }
+ })
+}
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi/eventhandler/httpresponse.md b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
index 8d0f7d0ba..c1324758c 100644
--- a/docs/javascript-api/mokapi/eventhandler/httpresponse.md
+++ b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
@@ -1,17 +1,66 @@
---
title: HttpResponse
-description: HttpResponse is an object used by HttpEventHandler
+description: HttpResponse is an object that is used to construct the HTTP response in an HttpEventHandler.
---
# HttpResponse
-HttpResponse is an object used by [HttpEventHandler](/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md)
-that contains response-specific data such as HTTP headers.
+`HttpResponse` is an object passed to an
+[`HttpEventHandler`](/docs/javascript-api/mokapi/eventhandler/httpeventhandler.md).
+It is used to define the outgoing HTTP response, including status code, headers,
+and response body.
-| Name | Type | Description |
-|------------|--------|--------------------------------------------------------------------------|
-| statusCode | number | Specifies the http status used to select the OpenAPI response definition |
-| headers | object | Object contains header parameters specified by OpenAPI header parameters |
-| body | string | Response body. It has a higher precedence than data |
-| data | any | Data will be encoded with the OpenAPI response definition |
+| Name | Type | Description |
+|------------|--------|------------------------------------------------------------------------------|
+| statusCode | number | HTTP status code used to select the OpenAPI response definition |
+| headers | object | Response headers defined by the OpenAPI response header parameters |
+| body | string | Raw response body. Takes precedence over `data` |
+| data | any | Response data that will be encoded according to the OpenAPI response schema |
+## Default Response Generation
+Mokapi automatically generates a valid HTTP response based on the OpenAPI
+specification.
+
+- The HTTP status code is automatically selected as the **first successful
+ response (`200–299`) defined in the OpenAPI specification**, in the order they appear
+- The response data is generated with valid example data according to the schema
+ defined for the selected status code.
+- Response headers defined in the OpenAPI specification are also generated with
+ valid values
+
+For example, if the OpenAPI specification defines a `201` response before a
+`200` response, Mokapi selects `201` and generates the response body and headers
+based on the schema defined for that status code.
+
+This behavior ensures that every response is valid according to the OpenAPI
+definition, even if the event handler does not explicitly modify the response.
+
+## Example
+
+The following example demonstrates how to construct an HTTP response inside an
+HTTP event handler.
+
+```javascript
+import { on } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ response.statusCode = 200
+ response.headers = {
+ 'Content-Type': 'application/json'
+ }
+ response.data = {
+ message: 'Hello World'
+ }
+ })
+}
+```
+
+## Body vs Data
+
+Use body to return a raw response body without OpenAPI encoding and validating.
+
+Use data to return structured data that should be validated and encoded
+according to the OpenAPI response definition
+
+If both body and data are set, body takes precedence
\ No newline at end of file
diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts
index 42d6a7447..a9c342cf7 100644
--- a/npm/types/index.d.ts
+++ b/npm/types/index.d.ts
@@ -157,7 +157,7 @@ export interface HttpRequest {
/** Object contains querystring parameters specified by OpenAPI querystring parameters. */
readonly querystring: any;
- /** The title of the API from the OpenAPI specification */
+ /** Name of the API, as defined in the OpenAPI `info.title` field */
readonly api: string
/** Path value specified by the OpenAPI path */
From 4e8f1f094638b9e8a9efbe0d2f25745cda3a6341 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 20:09:03 +0100
Subject: [PATCH 62/75] feat(generator): add message
---
schema/json/generator/text.go | 7 ++++++-
schema/json/generator/text_test.go | 16 ++++++++++++++--
2 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/schema/json/generator/text.go b/schema/json/generator/text.go
index fa243d96d..4b17c09b7 100644
--- a/schema/json/generator/text.go
+++ b/schema/json/generator/text.go
@@ -1,8 +1,9 @@
package generator
import (
- "github.com/brianvoe/gofakeit/v6"
"math"
+
+ "github.com/brianvoe/gofakeit/v6"
)
func textNodes() []*Node {
@@ -15,6 +16,10 @@ func textNodes() []*Node {
Name: "category",
Fake: fakeCategory,
},
+ {
+ Name: "message",
+ Fake: fakeDescription,
+ },
}
}
diff --git a/schema/json/generator/text_test.go b/schema/json/generator/text_test.go
index 1cdc4dbcd..4e1efafef 100644
--- a/schema/json/generator/text_test.go
+++ b/schema/json/generator/text_test.go
@@ -1,10 +1,11 @@
package generator
import (
- "github.com/brianvoe/gofakeit/v6"
- "github.com/stretchr/testify/require"
"mokapi/schema/json/schema/schematest"
"testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/stretchr/testify/require"
)
func TestStringDescription(t *testing.T) {
@@ -37,6 +38,17 @@ func TestStringDescription(t *testing.T) {
require.Equal(t, "Say just these run whose foot this least.", v)
},
},
+ {
+ name: "message",
+ req: &Request{
+ Path: []string{"message"},
+ Schema: schematest.New("string"),
+ },
+ test: func(t *testing.T, v interface{}, err error) {
+ require.NoError(t, err)
+ require.Equal(t, "Ourselves whomever wade regularly you how theirs these tomorrow staff gloves wow then opposite conclude those abroad she stop mob a rubbish mob as.", v)
+ },
+ },
}
for _, tc := range testcases {
From 100474902d26ab74727c192d247b782e56dfe443 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 20:35:33 +0100
Subject: [PATCH 63/75] feat(javascript): add HttpResponse.rebuild()
---
.../mokapi/eventhandler/httpresponse.md | 100 ++++++++--
engine/common/http.go | 2 +
js/mokapi/on.go | 29 ++-
js/mokapi/on_test.go | 112 +++++++++++
js/mokapi/proxy.go | 32 +++-
lua/convert/convert.go | 9 +-
providers/openapi/handler.go | 43 +++++
providers/openapi/handler_test.go | 177 +++++++++++++++++-
8 files changed, 473 insertions(+), 31 deletions(-)
diff --git a/docs/javascript-api/mokapi/eventhandler/httpresponse.md b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
index c1324758c..0733c6586 100644
--- a/docs/javascript-api/mokapi/eventhandler/httpresponse.md
+++ b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
@@ -9,6 +9,8 @@ description: HttpResponse is an object that is used to construct the HTTP respon
It is used to define the outgoing HTTP response, including status code, headers,
and response body.
+## Properties
+
| Name | Type | Description |
|------------|--------|------------------------------------------------------------------------------|
| statusCode | number | HTTP status code used to select the OpenAPI response definition |
@@ -16,6 +18,33 @@ and response body.
| body | string | Raw response body. Takes precedence over `data` |
| data | any | Response data that will be encoded according to the OpenAPI response schema |
+## Methods
+
+| Name | Description |
+|----------------------------------|------------------------------|
+| rebuild(statusCode, contentType) | Rebuilds the HTTP response |
+
+## Example
+
+The following example demonstrates how to construct an HTTP response inside an
+HTTP event handler.
+
+```javascript
+import { on } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ response.statusCode = 200
+ response.headers = {
+ 'Content-Type': 'application/json'
+ }
+ response.data = {
+ message: 'Hello World'
+ }
+ })
+}
+```
+
## Default Response Generation
Mokapi automatically generates a valid HTTP response based on the OpenAPI
@@ -35,32 +64,75 @@ based on the schema defined for that status code.
This behavior ensures that every response is valid according to the OpenAPI
definition, even if the event handler does not explicitly modify the response.
-## Example
+## Body vs Data
-The following example demonstrates how to construct an HTTP response inside an
-HTTP event handler.
+Use body to return a raw response body without OpenAPI encoding and validating.
+
+Use data to return structured data that should be validated and encoded
+according to the OpenAPI response definition
+
+If both body and data are set, body takes precedence
+
+## Rebuilding a Response
+
+When changing the HTTP status code or content type, the existing response data
+and headers may no longer match the OpenAPI specification.
+
+To address this, `HttpResponse` provides a helper function:
+
+```typescript title=Definition
+function rebuild(statusCode?: number, contentType?: string): void {}
+```
+
+This function rebuilds the entire HTTP response using the OpenAPI response
+definition for the given status code and content type.
+
+### What rebuild does
+
+Calling rebuild will:
+- Select the matching response definition from the OpenAPI specification
+- Set response.statusCode to the provided value
+- Generate valid response data based on the response schema
+- Generate valid response headers defined in the specification
+- Replace previously generated response data and headers
+
+This ensures the response remains valid after changing the status code.
+
+### When to Use rebuild
+
+Use rebuild when:
+- You change the response status code
+- You want to switch to a different response definition
+- You want Mokapi to regenerate valid example data and headers
+
+You do not need to call rebuild if you only modify fields within the
+already generated response.data.
+
+### Example: Changing the Status Code Safely
```javascript
import { on } from 'mokapi'
export default function() {
on('http', (request, response) => {
- response.statusCode = 200
- response.headers = {
- 'Content-Type': 'application/json'
- }
- response.data = {
- message: 'Hello World'
+ if (request.path.petId === 10) {
+ // Switch to a different OpenAPI response
+ response.rebuild(404, 'application/json')
+
+ // Modify only what matters
+ response.data.message = 'Pet not found'
}
})
}
```
-## Body vs Data
+### Parameter Defaults
-Use body to return a raw response body without OpenAPI encoding and validating.
+- If `statusCode` is not provided, Mokapi selects the OpenAPI `default` response.
+- If `contentType` is not provided, Mokapi selects the first content type
+ defined for the selected status code in the OpenAPI specification.
-Use data to return structured data that should be validated and encoded
-according to the OpenAPI response definition
+### Error handling
-If both body and data are set, body takes precedence
\ No newline at end of file
+If `response.rebuild()` throws an error, and it is not caught, the current event
+handler is skipped and no response modifications from that handler are applied.
\ No newline at end of file
diff --git a/engine/common/http.go b/engine/common/http.go
index 3056a2063..b9519e9d3 100644
--- a/engine/common/http.go
+++ b/engine/common/http.go
@@ -11,6 +11,8 @@ type HttpEventResponse struct {
StatusCode int `json:"statusCode"`
Body string `json:"body"`
Data any `json:"data"`
+
+ Rebuild func(statusCode int, contentType string) `json:"-"`
}
type HttpEventRequest struct {
diff --git a/js/mokapi/on.go b/js/mokapi/on.go
index 3242eddc1..2fb2407ce 100644
--- a/js/mokapi/on.go
+++ b/js/mokapi/on.go
@@ -122,7 +122,7 @@ func getHashes(args ...any) ([][]byte, error) {
for _, arg := range args {
b, err := json.Marshal(arg)
if err != nil {
- return nil, fmt.Errorf("unable to marshal arg")
+ return nil, fmt.Errorf("failed to marshal arg: %v", err)
}
result = append(result, b)
}
@@ -151,6 +151,8 @@ func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
switch key {
case "headers":
p.KeyNormalizer = http.CanonicalHeaderKey
+ case "rebuild":
+ return rebuild(vm, v)
}
switch val.(type) {
@@ -165,3 +167,28 @@ func ArgToJs(arg any, vm *goja.Runtime) goja.Value {
return vm.ToValue(v)
}
}
+
+func rebuild(vm *goja.Runtime, res *common.HttpEventResponse) goja.Value {
+ if res.Rebuild == nil {
+ return vm.ToValue(func() {})
+ }
+ return vm.ToValue(func(statusCode goja.Value, contentType goja.Value) {
+ s := int64(0)
+ c := ""
+ if statusCode != nil {
+ if statusCode.ExportType().Kind() != reflect.Int64 {
+ panic(fmt.Sprintf("response.rebuild failed: statusCode must be a number: got %v", util.JsType(statusCode.Export())))
+ } else {
+ s = statusCode.ToInteger()
+ }
+ }
+ if contentType != nil {
+ if contentType.ExportType().Kind() != reflect.String {
+ panic(fmt.Sprintf("response.rebuild failed: contentType must be a string: got %v", util.JsType(contentType.Export())))
+ } else {
+ c = contentType.String()
+ }
+ }
+ res.Rebuild(int(s), c)
+ })
+}
diff --git a/js/mokapi/on_test.go b/js/mokapi/on_test.go
index 3208fa12c..c61560a61 100644
--- a/js/mokapi/on_test.go
+++ b/js/mokapi/on_test.go
@@ -490,6 +490,118 @@ m.on('http', (req, res) => {
r.Equal(t, map[string]any{"foo": "yuh"}, res.Data)
},
},
+ {
+ name: "rebuild function not defined",
+ script: `
+const m = require('mokapi')
+m.on('http', (req, res) => {
+ res.rebuild();
+})
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ return evt.Emit("http", &common.HttpEventRequest{}, res)
+ },
+ test: func(t *testing.T, actions []*common.Action, err error) {
+ r.NoError(t, err)
+
+ r.Nil(t, actions[0].Error)
+
+ var res *common.HttpEventResponse
+ err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ r.Equal(t, map[string]any{"foo": "bar"}, res.Data)
+ },
+ },
+ {
+ name: "rebuild function updates data",
+ script: `
+const m = require('mokapi')
+m.on('http', (req, res) => {
+ res.rebuild();
+})
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ res.Rebuild = func(statusCode int, contentType string) {
+ res.Data = map[string]any{"foo": "yuh"}
+ }
+ return evt.Emit("http", &common.HttpEventRequest{}, res)
+ },
+ test: func(t *testing.T, actions []*common.Action, err error) {
+ r.NoError(t, err)
+
+ r.Nil(t, actions[0].Error)
+
+ var res *common.HttpEventResponse
+ err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ r.Equal(t, map[string]any{"foo": "yuh"}, res.Data)
+ },
+ },
+ {
+ name: "rebuild function with parameters",
+ script: `
+const m = require('mokapi')
+m.on('http', (req, res) => {
+ res.rebuild(200, 'application/json');
+})
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ res.Rebuild = func(statusCode int, contentType string) {
+ res.Data = map[string]any{"statusCode": statusCode, "contentType": contentType}
+ }
+ return evt.Emit("http", &common.HttpEventRequest{}, res)
+ },
+ test: func(t *testing.T, actions []*common.Action, err error) {
+ r.NoError(t, err)
+
+ r.Nil(t, actions[0].Error)
+
+ var res *common.HttpEventResponse
+ err = json.Unmarshal([]byte(actions[0].Parameters[1].(string)), &res)
+ r.Equal(t, map[string]any{"statusCode": float64(200), "contentType": "application/json"}, res.Data)
+ },
+ },
+ {
+ name: "rebuild function wrong type statusCode",
+ script: `
+const m = require('mokapi')
+m.on('http', (req, res) => {
+ res.rebuild({ }, 'application/json');
+})
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ res.Rebuild = func(statusCode int, contentType string) {}
+ return evt.Emit("http", &common.HttpEventRequest{}, res)
+ },
+ test: func(t *testing.T, actions []*common.Action, err error) {
+ r.NoError(t, err)
+
+ r.NotNil(t, actions[0].Error)
+ r.Equal(t, "response.rebuild failed: statusCode must be a number: got Object", actions[0].Error.Message)
+ },
+ },
+ {
+ name: "rebuild function wrong type contentType",
+ script: `
+const m = require('mokapi')
+m.on('http', (req, res) => {
+ res.rebuild(100, 200);
+})
+`,
+ run: func(evt common.EventEmitter) []*common.Action {
+ res := &common.HttpEventResponse{Data: map[string]any{"foo": "bar"}}
+ res.Rebuild = func(statusCode int, contentType string) {}
+ return evt.Emit("http", &common.HttpEventRequest{}, res)
+ },
+ test: func(t *testing.T, actions []*common.Action, err error) {
+ r.NoError(t, err)
+
+ r.NotNil(t, actions[0].Error)
+ r.Equal(t, "response.rebuild failed: contentType must be a string: got Integer", actions[0].Error.Message)
+ },
+ },
}
for _, tc := range testcases {
diff --git a/js/mokapi/proxy.go b/js/mokapi/proxy.go
index 32d3678b8..374343020 100644
--- a/js/mokapi/proxy.go
+++ b/js/mokapi/proxy.go
@@ -54,8 +54,10 @@ func (p *Proxy) Get(key string) goja.Value {
v := target.MapIndex(reflect.ValueOf(key))
return p.toJSValue(key, v)
case reflect.Struct:
- f := getFieldByTag(target, key, "json")
- return p.toJSValue(key, f)
+ f := getField(target, key, "json")
+ if f.IsValid() {
+ return p.toJSValue(key, f)
+ }
case reflect.Slice:
switch key {
case "length":
@@ -116,9 +118,8 @@ func (p *Proxy) Get(key string) goja.Value {
}
return goja.Undefined()
}
- default:
- return goja.Undefined()
}
+ panic(fmt.Sprintf("%s is not defined", key))
}
func (p *Proxy) Has(key string) bool {
@@ -133,7 +134,7 @@ func (p *Proxy) Has(key string) bool {
k := target.MapIndex(reflect.ValueOf(key))
return k.IsValid()
case reflect.Struct:
- f := getFieldByTag(target, key, "json")
+ f := getField(target, key, "json")
return f.IsValid()
default:
return false
@@ -155,7 +156,7 @@ func (p *Proxy) Set(key string, value goja.Value) bool {
target.SetMapIndex(reflect.ValueOf(key), v)
return true
case reflect.Struct:
- f := getFieldByTag(target, key, "json")
+ f := getField(target, key, "json")
err := assignValue(f, value.Export(), key)
if err != nil {
panic(p.vm.ToValue(err))
@@ -216,6 +217,10 @@ func (p *Proxy) normalizeKey(key string) string {
}
func (p *Proxy) toJSValue(key string, v reflect.Value) goja.Value {
+ if !v.IsValid() {
+ return goja.Undefined()
+ }
+
if p.ToJSValue != nil {
return p.ToJSValue(p.vm, key, v.Interface())
}
@@ -242,10 +247,15 @@ func (p *Proxy) Export() any {
return Export(v)
}
-func getFieldByTag(structValue reflect.Value, name, tag string) reflect.Value {
+func getField(structValue reflect.Value, name, tag string) reflect.Value {
+ name = capitalize(name)
for i := 0; i < structValue.NumField(); i++ {
- v := structValue.Type().Field(i).Tag.Get(tag)
- tagValues := strings.Split(v, ",")
+ f := structValue.Type().Field(i)
+ if f.Name == name {
+ return structValue.Field(i)
+ }
+ t := f.Tag.Get(tag)
+ tagValues := strings.Split(t, ",")
for _, tagValue := range tagValues {
if tagValue == name {
return structValue.Field(i)
@@ -362,3 +372,7 @@ func unwrap(v reflect.Value) reflect.Value {
}
}
}
+
+func capitalize(s string) string {
+ return strings.ToUpper(s[0:1]) + s[1:]
+}
diff --git a/lua/convert/convert.go b/lua/convert/convert.go
index d3af2b4f2..49a39f0a1 100644
--- a/lua/convert/convert.go
+++ b/lua/convert/convert.go
@@ -2,10 +2,11 @@ package convert
import (
"fmt"
- lua "github.com/yuin/gopher-lua"
- luar "layeh.com/gopher-luar"
"mokapi/sortedmap"
"reflect"
+
+ lua "github.com/yuin/gopher-lua"
+ luar "layeh.com/gopher-luar"
)
func FromLua(lv lua.LValue, to interface{}) error {
@@ -102,6 +103,10 @@ func ToLua(l *lua.LState, from interface{}) (lua.LValue, error) {
if !f.IsExported() {
continue
}
+ tag := f.Tag.Get("json")
+ if tag == "-" {
+ continue
+ }
fields = append(fields, reflect.StructField{
Name: f.Name,
Type: reflect.TypeOf((*lua.LValue)(nil)).Elem(),
diff --git a/providers/openapi/handler.go b/providers/openapi/handler.go
index f573b5b52..2a4c70679 100644
--- a/providers/openapi/handler.go
+++ b/providers/openapi/handler.go
@@ -116,6 +116,7 @@ func (h *responseHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
response := NewEventResponse(status, contentType)
+ setResponseRebuild(response, request, op)
err = setResponseData(response, mediaType, request)
if err != nil {
@@ -464,3 +465,45 @@ func drainRequestBody(r *http.Request) {
log.Warnf("timeout reading request body for %s %s", r.Method, lib.GetUrl(r))
}
}
+
+func setResponseRebuild(response *common.HttpEventResponse, request *common.HttpEventRequest, op *Operation) {
+ response.Rebuild = func(statusCode int, contentType string) {
+ res := op.Responses.GetResponse(statusCode)
+ if res == nil {
+ res = op.getResponse(0)
+ if res == nil {
+ panic(
+ fmt.Sprintf(
+ "no configuration was found for HTTP status code %v, https://swagger.io/docs/specification/describing-responses",
+ statusCode,
+ ),
+ )
+ }
+ }
+ var mediaType *MediaType
+ if contentType != "" {
+ mediaType = res.Content[contentType]
+ if mediaType == nil {
+ panic(fmt.Sprintf("content type '%s' is not specified for HTTP status code %v", contentType, statusCode))
+ }
+ } else {
+ for name, mt := range res.Content {
+ contentType = name
+ mediaType = mt
+ }
+ }
+
+ response.StatusCode = statusCode
+ response.Headers = map[string]any{}
+
+ err := setResponseData(response, mediaType, request)
+ if err != nil {
+ panic(err)
+ }
+
+ err = setResponseHeader(response, res.Headers)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
diff --git a/providers/openapi/handler_test.go b/providers/openapi/handler_test.go
index 1f48b7adb..39c601995 100644
--- a/providers/openapi/handler_test.go
+++ b/providers/openapi/handler_test.go
@@ -3,6 +3,7 @@ package openapi_test
import (
"context"
"encoding/json"
+ "fmt"
"io"
"mokapi/config/dynamic"
"mokapi/engine/common"
@@ -1249,7 +1250,7 @@ func TestHandler_Event_TypeScript(t *testing.T) {
name: "async event handler",
test: func(t *testing.T) {
e := enginetest.NewEngine()
- err := e.AddScript(newScript("test.ts", `
+ err := e.AddScript(newScript(fmt.Sprintf("%s.ts", t.Name()), `
import {on, sleep} from 'mokapi'
export default function() {
on('http', async (request, response) => {
@@ -1273,7 +1274,9 @@ func TestHandler_Event_TypeScript(t *testing.T) {
}
h := func(rw http.ResponseWriter, r *http.Request) {
- h := openapi.NewHandler(config, e, &events.StoreManager{})
+ sm := &events.StoreManager{}
+ sm.SetStore(10, events.NewTraits().WithNamespace("http"))
+ h := openapi.NewHandler(config, e, sm)
err = h.ServeHTTP(rw, r)
require.Nil(t, err)
}
@@ -1289,14 +1292,178 @@ func TestHandler_Event_TypeScript(t *testing.T) {
require.Equal(t, `"foo"`, rr.Body.String())
},
},
+ {
+ name: "rebuild different status code",
+ test: func(t *testing.T) {
+ e := enginetest.NewEngine()
+ err := e.AddScript(newScript(fmt.Sprintf("%s.ts", t.Name()), `
+ import {on, sleep} from 'mokapi'
+ export default function() {
+ on('http', async (request, response) => {
+ response.rebuild(404);
+ });
+ }
+ `))
+ require.NoError(t, err)
+
+ config := &openapi.Config{
+ Info: openapi.Info{Name: "Testing"},
+ Servers: []*openapi.Server{{Url: "http://localhost"}},
+ }
+
+ h := func(rw http.ResponseWriter, r *http.Request) {
+ sm := &events.StoreManager{}
+ sm.SetStore(10, events.NewTraits().WithNamespace("http"))
+ h := openapi.NewHandler(config, e, sm)
+ err = h.ServeHTTP(rw, r)
+ require.Nil(t, err)
+ }
+
+ op := openapitest.NewOperation(
+ openapitest.WithResponse(http.StatusOK, openapitest.WithContent("application/json",
+ openapitest.NewContent(openapitest.WithSchema(
+ schematest.New(
+ "object",
+ schematest.WithProperty("foo", schematest.New("string")),
+ schematest.WithRequired("foo"),
+ ),
+ )),
+ )),
+ openapitest.WithResponse(http.StatusNotFound, openapitest.WithContent("application/json",
+ openapitest.NewContent(openapitest.WithSchema(
+ schematest.New(
+ "object",
+ schematest.WithProperty("message", schematest.New("string")),
+ schematest.WithRequired("message"),
+ ),
+ )),
+ )),
+ )
+ openapitest.AppendPath("/foo", config, openapitest.WithOperation("get", op))
+ r := httptest.NewRequest("get", "http://localhost/foo", nil)
+ r.Header.Set("accept", "application/json")
+ rr := httptest.NewRecorder()
+ h(rr, r)
+ require.Equal(t, http.StatusNotFound, rr.Code)
+ require.Contains(t, rr.Body.String(), `{"message":`)
+ },
+ },
+ {
+ name: "rebuild status code not in specification",
+ test: func(t *testing.T) {
+ e := enginetest.NewEngine()
+ err := e.AddScript(newScript(fmt.Sprintf("%s.ts", t.Name()), `
+ import {on, sleep} from 'mokapi'
+ export default function() {
+ on('http', async (request, response) => {
+ response.rebuild(404);
+ });
+ }
+ `))
+ require.NoError(t, err)
+
+ config := &openapi.Config{
+ Info: openapi.Info{Name: "Testing"},
+ Servers: []*openapi.Server{{Url: "http://localhost"}},
+ }
+
+ sm := &events.StoreManager{}
+ sm.SetStore(10, events.NewTraits().WithNamespace("http"))
+ h := func(rw http.ResponseWriter, r *http.Request) {
+ h := openapi.NewHandler(config, e, sm)
+ err = h.ServeHTTP(rw, r)
+ require.Nil(t, err)
+ }
+
+ op := openapitest.NewOperation(
+ openapitest.WithResponse(http.StatusOK, openapitest.WithContent("application/json",
+ openapitest.NewContent(openapitest.WithSchema(
+ schematest.New(
+ "object",
+ schematest.WithProperty("foo", schematest.New("string")),
+ schematest.WithRequired("foo"),
+ ),
+ )),
+ )),
+ )
+ openapitest.AppendPath("/foo", config, openapitest.WithOperation("get", op))
+ r := httptest.NewRequest("get", "http://localhost/foo", nil)
+ r.Header.Set("accept", "application/json")
+ rr := httptest.NewRecorder()
+ h(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Contains(t, rr.Body.String(), `{"foo":`)
+
+ result := sm.GetEvents(events.NewTraits().WithNamespace("http"))
+ log := result[0].Data.(*openapi.HttpLog)
+ require.Equal(t, "no configuration was found for HTTP status code 404, https://swagger.io/docs/specification/describing-responses", log.Actions[0].Error.Message)
+ },
+ },
+ {
+ name: "rebuild content type not in specification",
+ test: func(t *testing.T) {
+ e := enginetest.NewEngine()
+ err := e.AddScript(newScript(fmt.Sprintf("%s.ts", t.Name()), `
+ import {on, sleep} from 'mokapi'
+ export default function() {
+ on('http', async (request, response) => {
+ response.rebuild(404, 'text/plain');
+ });
+ }
+ `))
+ require.NoError(t, err)
+
+ config := &openapi.Config{
+ Info: openapi.Info{Name: "Testing"},
+ Servers: []*openapi.Server{{Url: "http://localhost"}},
+ }
+
+ sm := &events.StoreManager{}
+ sm.SetStore(10, events.NewTraits().WithNamespace("http"))
+ h := func(rw http.ResponseWriter, r *http.Request) {
+ h := openapi.NewHandler(config, e, sm)
+ err = h.ServeHTTP(rw, r)
+ require.Nil(t, err)
+ }
+
+ op := openapitest.NewOperation(
+ openapitest.WithResponse(http.StatusOK, openapitest.WithContent("application/json",
+ openapitest.NewContent(openapitest.WithSchema(
+ schematest.New(
+ "object",
+ schematest.WithProperty("foo", schematest.New("string")),
+ schematest.WithRequired("foo"),
+ ),
+ )),
+ )),
+ openapitest.WithResponse(http.StatusNotFound, openapitest.WithContent("application/json",
+ openapitest.NewContent(openapitest.WithSchema(
+ schematest.New(
+ "object",
+ schematest.WithProperty("message", schematest.New("string")),
+ schematest.WithRequired("message"),
+ ),
+ )),
+ )),
+ )
+ openapitest.AppendPath("/foo", config, openapitest.WithOperation("get", op))
+ r := httptest.NewRequest("get", "http://localhost/foo", nil)
+ r.Header.Set("accept", "application/json")
+ rr := httptest.NewRecorder()
+ h(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Contains(t, rr.Body.String(), `{"foo":`)
+
+ result := sm.GetEvents(events.NewTraits().WithNamespace("http"))
+ log := result[0].Data.(*openapi.HttpLog)
+ require.Equal(t, "content type 'text/plain' is not specified for HTTP status code 404", log.Actions[0].Error.Message)
+ },
+ },
}
- t.Parallel()
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
tc.test(t)
})
}
From 3c1f8bb3cef95e88441b9027835976146bd37056 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sat, 14 Feb 2026 22:32:45 +0100
Subject: [PATCH 64/75] doc: update documentation and TypeScript definition
---
docs/http/overview.md | 18 +++++++++++++++---
.../mokapi/eventhandler/httpresponse.md | 4 ++--
npm/types/index.d.ts | 17 +++++++++++++++++
3 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/docs/http/overview.md b/docs/http/overview.md
index 4dba64365..8cf1c48ae 100644
--- a/docs/http/overview.md
+++ b/docs/http/overview.md
@@ -347,14 +347,26 @@ This approach:
### Example: Modifying a generated response
```javascript
-import { on, sleep } from 'mokapi'
+import { on } from 'mokapi'
export default function() {
on('http', (request, response) => {
- if (request.key === '/pet/{petId}' && request.path.petId === 10) {
+ if (request.key === '/pet/{petId}') {
+
// Modify only the relevant part of the generated response
- response.data.name = 'Garfield'
+ if (request.path.petId === 10) {
+ response.data.name = 'Garfield'
+ return
+ }
+ // Safely switch to a different OpenAPI response
+ if (request.path.petId === 11) {
+ response.rebuild(404);
+ response.data.message = 'Pet not found'
+ }
}
})
}
```
+
+When switching the response status code, use [response.rebuild()](/docs/javascript-api/mokapi/eventhandler/httpresponse.md) to regenerate
+a valid response based on the OpenAPI specification before modifying specific fields.
\ No newline at end of file
diff --git a/docs/javascript-api/mokapi/eventhandler/httpresponse.md b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
index 0733c6586..fd772bda2 100644
--- a/docs/javascript-api/mokapi/eventhandler/httpresponse.md
+++ b/docs/javascript-api/mokapi/eventhandler/httpresponse.md
@@ -80,8 +80,8 @@ and headers may no longer match the OpenAPI specification.
To address this, `HttpResponse` provides a helper function:
-```typescript title=Definition
-function rebuild(statusCode?: number, contentType?: string): void {}
+```typescript tab=Definition
+rebuild(statusCode?: number, contentType?: string): void
```
This function rebuilds the entire HTTP response using the OpenAPI response
diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts
index a9c342cf7..a2976707b 100644
--- a/npm/types/index.d.ts
+++ b/npm/types/index.d.ts
@@ -186,6 +186,23 @@ export interface HttpResponse {
/** Data will be encoded with the OpenAPI response definition. */
data: any;
+
+ /**
+ * Rebuilds the entire HTTP response using the OpenAPI response definition for the given status code and content type
+ * @example
+ * import { on } from 'mokapi'
+ *
+ * export default function() {
+ * on('http', (request, response) => {
+ * if (request.path.petId === 10) {
+ * // Switch to a different OpenAPI response.
+ * response.rebuild(404, 'application/json')
+ * response.data.message = 'Pet not found'
+ * }
+ * })
+ * }
+ * */
+ rebuild: (statusCode?: number, contentType?: string) => void;
}
/**
From 0f1f28bf42eff9ac1ef301786261bf204650db8b Mon Sep 17 00:00:00 2001
From: maesi
Date: Sun, 15 Feb 2026 00:55:15 +0100
Subject: [PATCH 65/75] doc: update TypeScript definition documentation
---
npm/types/index.d.ts | 34 ++++++++++++++++++++++++++--------
1 file changed, 26 insertions(+), 8 deletions(-)
diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts
index a2976707b..bcbff1719 100644
--- a/npm/types/index.d.ts
+++ b/npm/types/index.d.ts
@@ -10,6 +10,7 @@ import "./mustache";
import "./yaml";
import "./encoding";
import "./mail";
+import "./file"
/**
* Attaches an event handler for the given event.
@@ -158,7 +159,7 @@ export interface HttpRequest {
readonly querystring: any;
/** Name of the API, as defined in the OpenAPI `info.title` field */
- readonly api: string
+ readonly api: string;
/** Path value specified by the OpenAPI path */
readonly key: string;
@@ -167,7 +168,7 @@ export interface HttpRequest {
readonly operationId: string;
/** Returns a string representing this HttpRequest object. */
- toString(): string
+ toString(): string;
}
/**
@@ -441,10 +442,11 @@ export type DateLayout =
| "RFC3339Nano";
/**
- * EventArgs object contains additional arguments for an event handler.
+ * EventArgs provides optional configuration for an event handler.
* https://mokapi.io/docs/javascript-api/mokapi/on
*
- * Use this to customize how the event appears in the dashboard or to control tracking.
+ * Use this object to control how the event is tracked, labeled,
+ * and ordered in the execution pipeline.
*
* @example
* export default function() {
@@ -458,16 +460,32 @@ export type DateLayout =
*/
export interface EventArgs {
/**
- * Adds or overrides existing tags used to label the event in dashboard
+ * Adds or overrides tags used to label this event in the dashboard.
+ * Tags can be used for filtering, grouping, or ownership attribution.
*/
tags?: { [key: string]: string };
/**
- * Set to `true` to enable tracking of this event handler in the dashboard.
- * Set to `false` to disable tracking. If omitted, Mokapi checks the response
- * object to determine if the handler changed it, and tracks it accordingly.
+ * Controls whether this event handler is tracked in the dashboard.
+ *
+ * - true: always track this handler
+ * - false: never track this handler
+ * - undefined: Mokapi determines tracking automatically based on
+ * whether the response object was modified by the handler
*/
track?: boolean;
+
+ /**
+ * Defines the execution order of the event handler.
+ *
+ * Handlers with higher priority values run first.
+ * Handlers with lower priority values run later.
+ *
+ * Use negative priorities (e.g. -1) to run a handler after
+ * the response has been fully populated by other handlers,
+ * such as for logging or recording purposes.
+ */
+ priority?: number;
}
/**
From 66bc7864c5d098a0e6709a7b2459e72c13fbe22b Mon Sep 17 00:00:00 2001
From: maesi
Date: Sun, 15 Feb 2026 13:34:06 +0100
Subject: [PATCH 66/75] doc: change h1 to h2
---
.github/actions/build-release-notes/action.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/actions/build-release-notes/action.yml b/.github/actions/build-release-notes/action.yml
index 34da9ee25..e50f4a591 100644
--- a/.github/actions/build-release-notes/action.yml
+++ b/.github/actions/build-release-notes/action.yml
@@ -9,7 +9,7 @@ runs:
VERSION=$(jq -r '.release.tag_name' "$GITHUB_EVENT_PATH")
BODY=$(jq -r '.release.body' "$GITHUB_EVENT_PATH")
{
- echo "# Mokapi $VERSION"
+ echo "## Mokapi $VERSION"
echo
echo "$BODY"
} > docs/release.md
From 2f6ebacef63dfdcc18ab2dcc12fe30e5cf1430d9 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sun, 15 Feb 2026 13:36:39 +0100
Subject: [PATCH 67/75] feat(webui): allow adding subtitles and tags to
documentation article
---
webui/src/composables/markdown-title.ts | 51 +++++++++++++++++++++++++
webui/src/composables/markdown.ts | 2 +
webui/src/types/index.d.ts | 2 +
webui/src/views/DocsView.vue | 23 ++++++++++-
4 files changed, 77 insertions(+), 1 deletion(-)
create mode 100644 webui/src/composables/markdown-title.ts
diff --git a/webui/src/composables/markdown-title.ts b/webui/src/composables/markdown-title.ts
new file mode 100644
index 000000000..ba0e2159d
--- /dev/null
+++ b/webui/src/composables/markdown-title.ts
@@ -0,0 +1,51 @@
+import type { Options } from "markdown-it";
+import type MarkdownIt from "markdown-it"
+import type Token from "markdown-it/lib/token.mjs";
+
+export function MarkdownItTitle(meta: DocMeta): (md: MarkdownIt, opts: Options) => void {
+
+ return function(md: MarkdownIt): void {
+
+ md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
+ const token = tokens[idx] as Token;
+
+ let heading = self.renderToken(tokens, idx, options);
+
+ if (token.tag !== 'h1') {
+ return heading;
+ }
+
+ let html = '';
+ if (meta) {
+ if (meta.tags) {
+ html += `
`
+ for (const tag of meta.tags) {
+ html += `${md.utils.escapeHtml(tag)}`
+ }
+ html += `
`
+ }
+ }
+
+ return html + heading;
+ }
+
+ md.renderer.rules.heading_close = (tokens, idx, options, env, self) => {
+ const token = tokens[idx] as Token;
+
+ let html = self.renderToken(tokens, idx, options);
+
+ if (token.tag !== 'h1') {
+ return html;
+ }
+
+ if (meta) {
+ if (meta.subtitle) {
+ html += `
${meta.subtitle}
`;
+ }
+ }
+
+ return html;
+ };
+
+ }
+}
\ No newline at end of file
diff --git a/webui/src/composables/markdown.ts b/webui/src/composables/markdown.ts
index e5fb3265b..213160973 100644
--- a/webui/src/composables/markdown.ts
+++ b/webui/src/composables/markdown.ts
@@ -8,6 +8,7 @@ import { MarkdownItCarousel } from './markdown-carousel';
import yaml from 'js-yaml'
import { MarkdownItBlockquote } from './markdown-blockquote';
import { MarkdownItTabContent } from './markdown-tab-content';
+import { MarkdownItTitle } from './markdown-title';
const images = import.meta.glob('/src/assets/docs/**/*.png', {as: 'url', eager: true})
const metadataRegex = /^---([\s\S]*?)---/;
@@ -25,6 +26,7 @@ export function useMarkdown(content: string | undefined): {content: string | und
if (content) {
content = new MarkdownIt()
.use(MarkdownItHighlightjs)
+ .use(MarkdownItTitle(metadata))
.use(MarkdownItBlockquote)
.use(MarkdownItTabs)
.use(MarkdownItTabContent)
diff --git a/webui/src/types/index.d.ts b/webui/src/types/index.d.ts
index 29a8ecf81..c4cc48764 100644
--- a/webui/src/types/index.d.ts
+++ b/webui/src/types/index.d.ts
@@ -27,6 +27,8 @@ interface DocMeta {
icon?: string
tech?: string
image?: { url: string, alt: string }
+ subtitle?: string
+ tags?: string[]
}
interface Source {
diff --git a/webui/src/views/DocsView.vue b/webui/src/views/DocsView.vue
index 866645204..723626a4e 100644
--- a/webui/src/views/DocsView.vue
+++ b/webui/src/views/DocsView.vue
@@ -26,7 +26,6 @@ const data = computed(() => {
return undefined;
}
const data = files[`/src/assets/docs/${current.value.source}`];
- console.log(data)
return useMarkdown(data)
})
@@ -533,4 +532,26 @@ a[name] {
.flags table tr th:nth-child(1){
width: 40%;
}
+
+.content p.subtitle {
+ margin-bottom: 1.6rem;
+ font-size: 1.2rem;
+ font-weight: 400;
+}
+
+.tags {
+ display: flex;
+ gap: 0.5rem;
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 1.5rem;
+}
+
+.tags li {
+ background: #eef2ff;
+ color: #3730a3;
+ padding: 0.2rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.8rem;
+}
\ No newline at end of file
From 5b04c2c8ab6729caf29698698c47da8f944c02f1 Mon Sep 17 00:00:00 2001
From: maesi
Date: Sun, 15 Feb 2026 13:41:53 +0100
Subject: [PATCH 68/75] doc: add documentation about recording HTTP requests
---
docs/config.json | 6 +
.../record-and-replay-api-interactions.md | 262 ++++++++++++++++++
webui/public/recording.png | Bin 0 -> 36916 bytes
3 files changed, 268 insertions(+)
create mode 100644 docs/resources/blogs/record-and-replay-api-interactions.md
create mode 100644 webui/public/recording.png
diff --git a/docs/config.json b/docs/config.json
index 835bcb454..92ec25bc3 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -646,6 +646,12 @@
"source": "resources/blogs/testing-kafka-workflows-playwright.md",
"path": "/resources/blogs/testing-kafka-workflows-playwright",
"hideNavigation": true
+ },
+ {
+ "label": "Record & Replay: API Interactions with Mokapi",
+ "source": "resources/blogs/record-and-replay-api-interactions.md",
+ "path": "/resources/blogs/record-and-replay-api-interactions",
+ "hideNavigation": true
}
]
}
diff --git a/docs/resources/blogs/record-and-replay-api-interactions.md b/docs/resources/blogs/record-and-replay-api-interactions.md
new file mode 100644
index 000000000..137efb217
--- /dev/null
+++ b/docs/resources/blogs/record-and-replay-api-interactions.md
@@ -0,0 +1,262 @@
+---
+title: "Record & Replay: API Interactions with Mokapi"
+description: Capture real-world API traffic for testing, debugging, and offline development—all with a simple JavaScript script
+subtitle: Capture real-world API traffic for testing, debugging, and offline development—all with a simple JavaScript script
+tech: http
+tags: ['HTTP']
+---
+
+# Record & Replay: API Interactions with Mokapi
+
+In a [previous article](ensuring-api-contract-compliance-with-mokapi.md), we explored how Mokapi can act as an API
+specification guard between services—validating requests and responses against your OpenAPI specs in real-time.
+But Mokapi's capabilities go far beyond validation. In this article, we'll dive into another powerful use case:
+recording and replaying API interactions.
+
+Imagine capturing real API traffic from your production or staging environment and replaying it later for testing,
+demos, or offline development. With Mokapi Script, this becomes remarkably straightforward.
+
+## Why Record API Interactions?
+
+Before we jump into the code, let's understand the value of recording API traffic:
+
+- **Complex User Scenarios**
+ Capture multistep workflows with real data to reproduce edge cases and validate business logic against actual usage patterns.
+- **Regression Testing**
+ Build a suite of real-world request/response pairs to ensure new changes don't break existing functionality.
+- **Demos & Presentations**
+ Replay realistic API interactions without depending on live services—perfect for sales demos or conference presentations.
+
+## The Recording Workflow
+
+At a high level, the recording workflow looks like this:
+
+
+
+## Building the Recording Script
+
+Let's build a script that records all HTTP traffic passing through Mokapi. The script will capture both requests
+and responses, storing them in a JSON file for later replay.
+
+### The Complete Recording Script
+
+```javascript
+import { on } from 'mokapi'
+import { read, writeString } from 'mokapi'
+
+export default function() {
+ on('http', (request, response) => {
+ // Initialize an empty array to hold recorded interactions
+ let data = []
+
+ try {
+ // Attempt to read existing recordings from file
+ const s = read('./recordings.json')
+ data = JSON.parse(s)
+ } catch {}
+
+ // Append the current request/response pair
+ data.push({ request, response })
+
+ // Write the updated recordings back to file
+ writeString('./recordings.json', JSON.stringify(data, null, 2))
+ }, {
+ // Use priority -1 to ensure this runs AFTER response is populated
+ priority: -1
+ })
+}
+```
+
+``` box=warning title="Understanding the Priority Setting"
+The priority: -1 parameter is crucial here. It ensures this handler runs after the response has been
+fully populated by other handlers (like your forwarding script or mock generators).
+```
+
+### How It Works
+
+Let's break down what's happening in this compact script:
+
+- **Event Listener:** The on('http', ...) function registers a handler that fires for every HTTP request passing through Mokapi.
+- **Read Existing Data:** We attempt to read recordings.json to retrieve previously recorded interactions. If the file doesn't
+ exist (first run), the try-catch silently handles it.
+- **Append New Data:** The current request/response pair is added to the array.
+- **Persist to Disk:** The entire array is serialized to JSON and written back to the file.
+
+## Combining Recording with Forwarding
+
+The real power emerges when you combine recording with the API forwarding pattern from our previous article.
+Here's how they work together:
+
+```javascript
+import { on } from 'mokapi'
+import { fetch } from 'mokapi/http'
+import { read, writeString } from 'mokapi'
+
+export default async function() {
+ // FIRST: Forward requests to real backend (default priority: 0)
+ on('http', async (request, response) => {
+ const url = getForwardUrl(request)
+
+ if (!url) {
+ response.statusCode = 500
+ response.body = 'Unknown backend'
+ return
+ }
+
+ try {
+ const res = await fetch(url, {
+ method: request.method,
+ body: request.body,
+ headers: request.header,
+ timeout: '30s'
+ })
+
+ response.statusCode = res.statusCode
+ response.headers = res.headers
+
+ const contentType = res.headers['Content-Type']?.[0] || ''
+ if (contentType.includes('application/json')) {
+ response.data = res.json()
+ } else {
+ response.body = res.body
+ }
+ } catch (e) {
+ response.statusCode = 500
+ response.body = e.toString()
+ }
+ })
+
+ // SECOND: Record the complete request/response (priority: -1)
+ on('http', (request, response) => {
+ let data = []
+ try {
+ data = JSON.parse(read('./recordings.json'))
+ } catch {}
+
+ data.push({ request, response })
+ writeString('./recordings.json', JSON.stringify(data, null, 2))
+ }, { priority: -1 })
+
+ // Helper function to determine backend URL
+ function getForwardUrl(request: HttpRequest): string | undefined {
+ switch (request.api) {
+ case 'backend-1':
+ return `https://backend1.example.com${request.url.path}?${request.url.query}`
+ case 'backend-2':
+ return `https://backend2.example.com${request.url.path}?${request.url.query}`
+ default:
+ return undefined
+ }
+ }
+}
+```
+
+This setup gives you the best of both worlds: real backend responses validated against your OpenAPI specs,
+plus a growing library of recorded interactions for later use.
+
+``` box=info
+When Mokapi forwards and records traffic, it still validates requests and
+responses against the OpenAPI specification whenever possible. This means you
+only record interactions that conform to your API contract—making replays
+reliable and spec-compliant.
+```
+
+## Building the Replay Script
+
+Now comes the fun part: replaying your recorded interactions! Here's a script that reads the recorded data
+and matches incoming requests to previously captured responses:
+
+```javascript
+import { on } from 'mokapi'
+import { read } from 'mokapi'
+
+export default function() {
+ // Load all recorded interactions at startup
+ let recordings = []
+ try {
+ const data = read('./recordings.json')
+ recordings = JSON.parse(data)
+ } catch (e) {
+ console.error('Failed to load recordings:', e)
+ return
+ }
+
+ on('http', (request, response) => {
+ // Find matching recorded request
+ const match = recordings.find(r =>
+ r.request.method === request.method &&
+ r.request.url.path === request.url.path &&
+ r.request.url.query === request.url.query
+ )
+
+ if (match) {
+ // Replay the recorded response
+ response.statusCode = match.response.statusCode
+ response.headers = match.response.headers
+ response.body = match.response.body
+ response.data = match.response.data
+ } else {
+ // No recording found
+ response.statusCode = 404
+ response.body = 'No recording found for this request'
+ }
+ })
+}
+```
+
+### Enhancing the Replay Logic
+
+The basic matching logic above works for simple cases, but you might want to enhance it for production use:
+
+- **Ignore specific headers**: Filter out timestamp headers or request IDs that change between requests
+- **Match on request body**: For POST/PUT requests, compare the request body to find the right response
+- **Fuzzy matching:** Match path patterns instead of exact paths (e.g., /users/:id)
+- **Sequential playback:** Replay requests in the order they were recorded for complex workflows
+
+## Practical Use Cases
+
+### 1. Building Regression Test Suites
+
+Record interactions from your staging environment, then replay them in CI/CD pipelines to ensure new
+code doesn't break existing functionality:
+
+> Record once in staging → Replay thousands of times in CI/CD → Catch regressions early
+
+### 2. Offline Development Environments
+
+Developers can work without VPN access or network connectivity. Simply record a comprehensive set of
+API interactions once, and your entire team can develop offline using the replay script.
+
+### 3. Demo Environments
+
+Create polished demos with predictable responses. No more crossed fingers hoping the backend behaves
+during that critical sales pitch!
+
+### 4. Performance Testing
+
+Replay recorded traffic to load-test your services without hitting real backends. Modify the
+replay script to simulate concurrent users.
+
+## Best Practices & Tips
+
+- **Sanitize sensitive data:** Before committing recordings to version control, strip out authentication tokens, personal information, and other sensitive data.
+- **Organize by scenario:** Instead of one giant recordings.json, create separate recording files for different user journeys or test scenarios.
+- **Version your recordings:** As your API evolves, maintain recordings for different API versions to support backward compatibility testing.
+- **Combine with validation:** Use Mokapi's OpenAPI validation alongside recording to ensure you're capturing valid interactions.
+
+## Taking It Further
+
+The recording and replay pattern opens up even more possibilities:
+
+- **Record/Replay UI:** Build a simple web interface to browse, filter, and selectively replay specific recordings
+- **Dynamic modification:** Modify responses on-the-fly during replay to test error handling or edge cases
+- **Traffic analysis:** Analyze recorded traffic to identify patterns, optimize API design, or detect anomalies
+- **Mock generation:** Use recorded interactions to automatically generate OpenAPI examples or mock data
+
+## Start Recording Today
+
+By combining OpenAPI validation, HTTP forwarding, and file-based recording,
+Mokapi becomes more than a mock server—it becomes a control plane for API
+interactions across the entire API lifecycle.
+
+> Record real user behavior once. Replay it endlessly. Evolve your APIs with confidence.
\ No newline at end of file
diff --git a/webui/public/recording.png b/webui/public/recording.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec9bbb654dee99ab1eba51012b0c3f095a07d3b5
GIT binary patch
literal 36916
zcmeFZWn5I-8#X*cDkzOXcPS;^h$A2kQi7yPcSx6jfJ#Y;lz>P{Nq38YG)OZ@clR(e
z?;89+AI|gf{rLXg>vufo2($O>z4lu7y6@}0t{tJFrf`#hiU0zE+2xiI)Rh5m*CTWvOT)(*^WHTVq%fgKpY)*EEg
zdjC3!(h#j*G}mG9c^nN}@|V7X7J}CpZ7WC>DbMWTW)$6LQ9fxW>yLr7h9L%opH$f5
zx>-EjV54bwBIUD(u7zAdaU9K+pp>K4O=~p%qSBy9_g+jA2Kb}n7?dN~nNAOnF~DtW
z#~`Q%15w$0ZLWk_tItkJ>aYxHzl)>wk(Pzd_qTbCZ`e)#c-5Q8ExAt;yGp7q>xVem
zp04I~8aMKobsslsnJ9bF6-7(PK|v5I$LJA1_ee!Fd>g&emm=agcSBGFgI-!%(pXrL
zZsC}X>EM@#!FV^Zc4}ACeRgW`?@0Te?fq?Ej_OCGN_rhEb;q@M!2M2VJ>XEoYKPf6
z_j{(oDd6^JhDb@DRDk;_#C>~{TG(!aZ+^~axAAn&CkTp-cjFG9?9N)lz~A;TVtVQO
zmFW7{+f$Xcw@u6R@85cVb?lLMqj3p-aWm>-v!t1>X158kQ@<8PWOwT}}?sJ@F*{{e?Jn>GP#H^{Om%-cP}jgZx{pEpRKUKMvw7Rr
zWVz))I=6kjfz0D1Wmv$`kOIBm5$WETo{8V_nBFjIf3Yi?UWU%|pqo9z_uz>&mn3Oy
zE(!0~ByPhhyU7RnkBqiHDLug;_M|*yv$g}OnDOs!*|zBgEPj9De-T*gy4s&6aXb7S
z1|NCVn6j!mf}SkgD6Ych>`Znhymeo{)D$Z5b&8CfL^+AasLn0FB4GIIlQ=e2EV_Wp
z{Vw`|=UkzRuqDopsQkz8Z*dz`D2fw@HwrGeZ`qbKo=n-6xHl5z7YaGfHFzDaKrvIf
z)GYW28J>_s=eyU6Pw%Ti