diff --git a/.asyncapi-tool.json b/.asyncapi-tool.json index c2ddc1965..b04b0f9f0 100644 --- a/.asyncapi-tool.json +++ b/.asyncapi-tool.json @@ -3,7 +3,7 @@ "description": "Your API Mocking Tool for Agile Development. Mock and simulate AsyncAPI and OpenAPI services for local testing, development, and CI pipelines — with a visual dashboard to inspect your mocks and a powerful test data generator fully customizable with JavaScript.", "links": { "websiteUrl": "https://mokapi.io", - "docsUrl": "https://mokapi.io/docs/guides/welcome", + "docsUrl": "https://mokapi.io/docs/welcome", "repoUrl": "https://github.com/marle3003/mokapi" }, "filters": { diff --git a/.github/actions/build-cli-flags-doc/action.yaml b/.github/actions/build-cli-flags-doc/action.yaml index eb0df5230..dd6b09b94 100644 --- a/.github/actions/build-cli-flags-doc/action.yaml +++ b/.github/actions/build-cli-flags-doc/action.yaml @@ -2,17 +2,11 @@ name: "Build Mokapi CLI Flags doc" runs: using: composite steps: - - name: Create directory and file + - name: Create dist directory and file shell: bash run: | mkdir -p webui/dist touch webui/dist/.tmp - name: Build CLI doc shell: bash - run: go run ./cmd/internal/gen-cli-docs/main.go - - name: Add CLI Flags nav entry - working-directory: ./docs - shell: bash - run: | - jq '.Configuration.items.Static.items["CLI Flags"] = "configuration/static/mokapi.md"' ./config.json > ./tmp.json - mv ./tmp.json ./config.json \ No newline at end of file + run: go run ./cmd/internal/gen-cli-docs/main.go \ No newline at end of file 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 diff --git a/.github/actions/publish-website/action.yml b/.github/actions/publish-website/action.yml index 569e9dbe7..7c5c9c6a4 100644 --- a/.github/actions/publish-website/action.yml +++ b/.github/actions/publish-website/action.yml @@ -51,25 +51,12 @@ runs: if: always() run: docker stop mokapi || true shell: bash - - name: build webui + - uses: ./.github/actions/build-cli-flags-doc + - name: build website working-directory: ./webui run: | npm install npm version ${{ steps.release.outputs.release }} - npm run build - shell: bash - - name: Build CLI doc - run: go run ./cmd/internal/gen-cli-docs/main.go - shell: bash - - name: Add CLI Flags nav entry - run: | - jq '.Configuration.items.Static.items["CLI Flags"] = "configuration/static/mokapi.md"' ./config.json > ./tmp.json - mv ./tmp.json ./config.json - working-directory: ./docs - shell: bash - - name: build website - working-directory: ./webui - run: | npm run copy-docs npm run build-sitemap npm run build-website diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5e39cc46..49ee05b56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version: 1.25.5 - - uses: ./.github/actions/build-cli-flags-doc - uses: ./.github/actions/build-release-notes - uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index 00da4ef74..05ff5d17b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@
Download · - Documentation + Documentation
# 🚀 Overview @@ -99,32 +99,32 @@ Mokapi’s dashboard lets you visualize your mock APIs. View requests and respon Explore tutorials that walk you through mocking different protocols and scenarios: -- 🌍 [Get started with REST API](https://mokapi.io/docs/resources/tutorials/get-started-with-rest-api)\ +- 🌍 [Get started with REST API](https://mokapi.io/resources/tutorials/get-started-with-rest-api)\ This tutorial will show you how to mock a REST API using an OpenAPI specification. -- ⚡ [Mocking Kafka with AsyncAPI](https://mokapi.io/docs/resources/tutorials/get-started-with-kafka)\ +- ⚡ [Mocking Kafka with AsyncAPI](https://mokapi.io/resources/tutorials/get-started-with-kafka)\ Mocking a Kafka topic using Mokapi and verifying that a producer generates valid messages. -- 👨💻 [Mocking LDAP Authentication](https://mokapi.io/docs/resources/tutorials/mock-ldap-authentication-in-node)\ +- 👨💻 [Mocking LDAP Authentication](https://mokapi.io/resources/tutorials/mock-ldap-authentication-in-node)\ Simulate LDAP-based login flows, including group-based permissions. -- 📧 [Mocking SMTP Mail Servers](https://mokapi.io/docs/resources/tutorials/mock-smtp-server-send-mail-using-node)\ +- 📧 [Mocking SMTP Mail Servers](https://mokapi.io/resources/tutorials/mock-smtp-server-send-mail-using-node)\ Use Mokapi to simulate sending and receiving emails in Node.js apps. -- 🖥️ [End-to-End Testing with Jest and GitHub Actions](https://mokapi.io/docs/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline)\ +- 🖥️ [End-to-End Testing with Jest and GitHub Actions](https://mokapi.io/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline)\ Integrate Mokapi into your CI pipeline for full-stack E2E testing. -> More examples are available on [mokapi.io/docs/resources](https://mokapi.io/docs/resources) +> More examples are available on [mokapi.io/resources](https://mokapi.io/resources) # 📚 Documentation -- [Get Started](https://mokapi.io/docs/guides/welcome) -- [HTTP](https://mokapi.io/docs/guides/http) -- [Kafka](https://mokapi.io/docs/guides/kafka) -- [LDAP](https://mokapi.io/docs/guides/ldap) -- [SMTP](https://mokapi.io/docs/guides/mail) -- [Javascript API](https://mokapi.io/docs/javascript-api) -- [Resources](https://mokapi.io/docs/resources) +- [Get Started](https://mokapi.io/docs/welcome) +- [HTTP](https://mokapi.io/docs/http/overview) +- [Kafka](https://mokapi.io/docs/kafka/overview) +- [LDAP](https://mokapi.io/docs/ldap/overview) +- [SMTP](https://mokapi.io/docs/mail/overview) +- [Javascript API](https://mokapi.io/docs/javascript-api/overview) +- [Resources](https://mokapi.io/resources) # ☕ Support diff --git a/Taskfile.yml b/Taskfile.yml index 25e268494..be5e6ea05 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -28,10 +28,12 @@ tasks: GOOS: darwin GOARCH: arm64 build-vue-app: + deps: [ensure-dist-folder] dir: webui cmds: - npm run clean - npm run copy-docs + - go run ../cmd/internal/gen-cli-docs/main.go --output-dir ./webui/src/assets/docs/configuration/static - npm version {{.VERSION}} - npm run build-dashboard build-npm-package: @@ -64,5 +66,9 @@ tasks: env: GOOS: darwin GOARCH: arm64 + ensure-dist-folder: + cmds: + - mkdir -p webui/dist + - touch webui/dist/.tmp diff --git a/acceptance/ldap_test.go b/acceptance/ldap_test.go index 64d0e5ea3..575246ff9 100644 --- a/acceptance/ldap_test.go +++ b/acceptance/ldap_test.go @@ -20,7 +20,7 @@ type LdapSuite struct { func (suite *LdapSuite) SetupSuite() { cfg := static.NewConfig() cfg.Api.Port = try.GetFreePort() - cfg.Providers.File.Directories = []string{"./ldap"} + cfg.Providers.File.Directories = []static.FileConfig{{Path: "./ldap"}} suite.initCmd(cfg) // ensure scripts are executed time.Sleep(2 * time.Second) diff --git a/acceptance/mail_test.go b/acceptance/mail_test.go index 8895c87a3..994c9c61b 100644 --- a/acceptance/mail_test.go +++ b/acceptance/mail_test.go @@ -27,7 +27,7 @@ func (suite *MailSuite) SetupSuite() { wd, err := os.Getwd() require.NoError(suite.T(), err) cfg.ConfigFile = path.Join(wd, "mokapi.yaml") - cfg.Providers.File.Directories = []string{"./mail"} + cfg.Providers.File.Directories = []static.FileConfig{{Path: "./mail"}} cfg.Certificates.Static = []static.Certificate{ {Cert: "./mail/mail.mokapi.local.pem"}, } diff --git a/acceptance/petstore_test.go b/acceptance/petstore_test.go index 6b9733194..6e66831fc 100644 --- a/acceptance/petstore_test.go +++ b/acceptance/petstore_test.go @@ -24,7 +24,7 @@ type PetStoreSuite struct{ BaseSuite } func (suite *PetStoreSuite) SetupSuite() { cfg := static.NewConfig() cfg.Api.Port = try.GetFreePort() - cfg.Providers.File.Directories = []string{"./petstore"} + cfg.Providers.File.Directories = []static.FileConfig{{Path: "./petstore"}} cfg.Api.Search.Enabled = true suite.initCmd(cfg) } diff --git a/api/handler_config.go b/api/handler_config.go index bd205b620..aa761e090 100644 --- a/api/handler_config.go +++ b/api/handler_config.go @@ -2,13 +2,14 @@ package api import ( "fmt" - log "github.com/sirupsen/logrus" "mime" "mokapi/config/dynamic" "net/http" "path/filepath" "strings" "time" + + log "github.com/sirupsen/logrus" ) type config struct { @@ -17,6 +18,7 @@ type config struct { Provider string `json:"provider"` Time time.Time `json:"time"` Refs []configRef `json:"refs,omitempty"` + Tags []string `json:"tags,omitempty"` } type configRef struct { @@ -67,7 +69,7 @@ func (h *handler) getConfigData(w http.ResponseWriter, r *http.Request, key stri } token := r.Header.Get("If-None-Match") - checksum := fmt.Sprintf("%x", c.Info.Checksum) + checksum := fmt.Sprintf(`"%x"`, c.Info.Checksum) if token != "" && token == checksum { w.WriteHeader(http.StatusNotModified) return @@ -116,5 +118,6 @@ func toConfig(cfg *dynamic.Config) config { Time: cfg.Info.Time, Provider: cfg.Info.Provider, Refs: refs, + Tags: cfg.Info.Tags, } } diff --git a/api/handler_config_test.go b/api/handler_config_test.go index d7cd2ceae..f5abbd834 100644 --- a/api/handler_config_test.go +++ b/api/handler_config_test.go @@ -37,7 +37,7 @@ func TestHandler_Config(t *testing.T) { panic(err) } checksum := h.Sum(nil) - etag := fmt.Sprintf("%x", checksum) + etag := fmt.Sprintf(`"%x"`, checksum) testcases := []struct { name string diff --git a/api/handler_mail.go b/api/handler_mail.go index 86731fba5..50e9d4136 100644 --- a/api/handler_mail.go +++ b/api/handler_mail.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "mime" "mokapi/media" "mokapi/providers/mail" "mokapi/runtime" @@ -14,6 +15,8 @@ import ( "sort" "strings" "time" + + log "github.com/sirupsen/logrus" ) type mailSummary struct { @@ -420,7 +423,7 @@ func toMessage(m *smtp.Message) *messageData { MessageId: m.MessageId, InReplyTo: m.InReplyTo, Date: m.Date, - Subject: m.Subject, + Subject: decodeSmtpValue(m.Subject), ContentType: m.ContentType, ContentTransferEncoding: m.ContentTransferEncoding, Body: m.Body, @@ -453,9 +456,19 @@ func toAddress(list []smtp.Address) []address { var r []address for _, a := range list { r = append(r, address{ - Name: a.Name, + Name: decodeSmtpValue(a.Name), Address: a.Address, }) } return r } + +func decodeSmtpValue(s string) string { + dec := new(mime.WordDecoder) + r, err := dec.DecodeHeader(s) + if err != nil { + log.Errorf("failed to decode SMTP header: %v", err) + return s + } + return r +} diff --git a/api/handler_mail_test.go b/api/handler_mail_test.go index e30239d42..c99de6db7 100644 --- a/api/handler_mail_test.go +++ b/api/handler_mail_test.go @@ -330,6 +330,47 @@ func TestHandler_Smtp(t *testing.T) { contentType: "text/plain", responseBody: "foobar", }, + { + name: "get SMTP mail with encodings", + app: func() *runtime.App { + app := runtime.New(&static.Config{}) + app.Mail.Set("foo", &runtime.MailInfo{ + Config: &mail.Config{}, + Store: &mail.Store{ + Name: "foo", + Mailboxes: map[string]*mail.Mailbox{ + "alice@foo.bar": { + Folders: map[string]*mail.Folder{ + "Inbox": { + Messages: []*mail.Mail{ + { + Message: &smtp.Message{ + Sender: nil, + From: []smtp.Address{{Address: "bob@foo.bar"}}, + To: []smtp.Address{ + {Address: "juergen@example.com", Name: "=?UTF-8?Q?J=C3=BCrgen?="}, + }, + MessageId: "foo-1@mokapi.io", + Date: now, + Subject: "=?UTF-8?Q?=C2=A1Buenos_d=C3=ADas!?= and =?UTF-8?B?bW9rYXBp?=", + ContentType: "text/plain", + Body: "foobar", + Size: 10, + }, + }, + }, + }, + }, + }, + }, + }, + }) + return app + }, + requestUrl: "http://foo.api/api/services/mail/messages/foo-1@mokapi.io", + contentType: "application/json", + responseBody: fmt.Sprintf(`{"service":"foo","data":{"from":[{"address":"bob@foo.bar"}],"to":[{"name":"Jürgen","address":"juergen@example.com"}],"messageId":"foo-1@mokapi.io","date":"%v","subject":"¡Buenos días! and mokapi","contentType":"text/plain","body":"foobar","size":10}}`, now.Format(time.RFC3339Nano)), + }, } t.Parallel() diff --git a/cmd/mokapi/main_test.go b/cmd/mokapi/main_test.go index 08e8384b0..47acd92f1 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"}, @@ -38,6 +59,7 @@ providers: skipPrefix: - _ include: [] + exclude: [] git: urls: [] pullInterval: "" @@ -84,6 +106,7 @@ data-gen: skipPrefix: - _ include: [] + exclude: [] git: urls: [] pullInterval: "" diff --git a/config/decoders/decoder.go b/config/decoders/decoder.go deleted file mode 100644 index 0f069dbae..000000000 --- a/config/decoders/decoder.go +++ /dev/null @@ -1,21 +0,0 @@ -package decoders - -type ConfigDecoder interface { - Decode(flags map[string][]string, element interface{}) error -} - -func Load(decoders []ConfigDecoder, config interface{}) error { - flags, err := parseFlags() - if err != nil { - return err - } - - for _, decoder := range decoders { - err := decoder.Decode(flags, config) - if err != nil { - return err - } - } - - return nil -} diff --git a/config/decoders/decoder_file.go b/config/decoders/decoder_file.go deleted file mode 100644 index d926448f2..000000000 --- a/config/decoders/decoder_file.go +++ /dev/null @@ -1,234 +0,0 @@ -package decoders - -import ( - "encoding/json" - "fmt" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "gopkg.in/yaml.v3" - "os" - "path/filepath" - "reflect" -) - -var ( - searchPaths = []string{".", "/etc/mokapi"} - fileNames = []string{"mokapi.yaml", "mokapi.yml"} -) - -type ReadFileFS func(path string) ([]byte, error) - -type FileDecoder struct { - filename string - readFile ReadFileFS -} - -func NewDefaultFileDecoder() *FileDecoder { - return NewFileDecoder(os.ReadFile) -} - -func NewFileDecoder(readFile ReadFileFS) *FileDecoder { - return &FileDecoder{readFile: readFile} -} - -func (f *FileDecoder) Decode(flags map[string][]string, element interface{}) error { - if len(f.filename) == 0 { - if val, ok := flags["configfile"]; ok { - delete(flags, "configfile") - f.filename = val[0] - } else if val, ok := flags["config-file"]; ok { - delete(flags, "config-file") - f.filename = val[0] - } else if val, ok := flags["cli-input"]; ok { - delete(flags, "cli-input") - f.filename = val[0] - } - } - - if len(f.filename) > 0 { - return f.read(f.filename, element) - } - - for _, dir := range searchPaths { - for _, name := range fileNames { - path := filepath.Join(dir, name) - if err := f.read(path, element); err == nil { - return nil - } else if !os.IsNotExist(err) { - return err - } - } - } - - return nil -} - -func (f *FileDecoder) read(path string, element interface{}) error { - data, err := f.readFile(path) - if err != nil { - return err - } - switch filepath.Ext(path) { - case ".yaml", ".yml": - err = unmarshalYaml(data, reflect.ValueOf(element)) - case ".json": - err = unmarshalJson(data, reflect.ValueOf(element)) - default: - err = fmt.Errorf("unsupported file extension: %v", filepath.Ext(path)) - } - - if err != nil { - return fmt.Errorf("parse file '%v' failed: %w", path, err) - } - return nil -} - -func unmarshalYaml(b []byte, element reflect.Value) error { - m := map[string]interface{}{} - err := yaml.Unmarshal(b, m) - if err != nil { - return err - } - - return mapConfig(m, element, "yaml") -} - -func unmarshalJson(b []byte, element reflect.Value) error { - m := map[string]interface{}{} - err := yaml.Unmarshal(b, m) - if err != nil { - return err - } - - return mapConfig(m, element, "json") -} - -var caser = cases.Title(language.English) - -func mapConfig(value interface{}, element reflect.Value, format string) (err error) { - defer func() { - r := recover() - if r != nil { - err = fmt.Errorf("cannot unmarshal %v into %v", toTypeName(reflect.ValueOf(value)), toTypeName(element)) - } - }() - - switch element.Type().Kind() { - case reflect.Pointer: - if element.IsNil() { - element.Set(reflect.New(element.Type().Elem())) - } - return mapConfig(value, element.Elem(), format) - case reflect.Bool, reflect.Int, reflect.Float64: - element.Set(reflect.ValueOf(value)) - case reflect.Int64: - switch i := value.(type) { - case int: - element.SetInt(int64(i)) - case int64: - element.SetInt(i) - default: - return fmt.Errorf("cannot unmarshal %v into %v", toTypeName(reflect.ValueOf(value)), toTypeName(element)) - } - - case reflect.String: - if _, ok := value.(string); ok { - t := element.Type() - if !reflect.TypeOf(value).AssignableTo(t) { - element.Set(reflect.ValueOf(value).Convert(t)) - } else { - element.Set(reflect.ValueOf(value)) - } - } else { - var b []byte - b, err = json.Marshal(value) - if err != nil { - return - } - element.Set(reflect.ValueOf(string(b))) - } - case reflect.Slice: - v := reflect.ValueOf(value) - if v.Type().Kind() != reflect.Slice { - ptr := reflect.New(element.Type().Elem()) - err = mapConfig(value, ptr.Elem(), format) - if err != nil { - return - } - element.Set(reflect.Append(element, ptr.Elem())) - } else { - arr, ok := value.([]interface{}) - if !ok { - return fmt.Errorf("expected array, got: %v", value) - } - for _, item := range arr { - err = mapConfig(item, element, format) - if err != nil { - return - } - } - } - case reflect.Struct: - m, ok := value.(map[string]interface{}) - if !ok { - return fmt.Errorf("expected object structure, got: %v", value) - } - for k, v := range m { - f := getFieldByTag(element, k, format) - if f.IsValid() { - err = mapConfig(v, f, format) - if err != nil { - return - } - continue - } - name := caser.String(k) - f = element.FieldByNameFunc(func(f string) bool { return f == name }) - if f.IsValid() { - err = mapConfig(v, f, format) - if err != nil { - return - } - continue - } - f = getFieldByTag(element, k, "explode") - if f.IsValid() { - err = mapConfig(v, f, format) - if err != nil { - return - } - } - } - case reflect.Map: - m, ok := value.(map[string]interface{}) - if !ok { - return fmt.Errorf("expected object structure, got: %v", value) - } - if element.IsNil() { - element.Set(reflect.MakeMap(element.Type())) - } - for k, v := range m { - ptr := reflect.New(element.Type().Elem()) - err = mapConfig(v, ptr.Elem(), format) - if err != nil { - return - } - element.SetMapIndex(reflect.ValueOf(k), ptr.Elem()) - } - default: - return fmt.Errorf("type not supported: %v", element.Type().Kind()) - } - - return -} - -func toTypeName(v reflect.Value) string { - switch v.Type().Kind() { - case reflect.Slice: - return "array" - case reflect.Struct, reflect.Map: - return "object" - default: - return v.Type().Kind().String() - } -} diff --git a/config/decoders/decoder_file_test.go b/config/decoders/decoder_file_test.go deleted file mode 100644 index a3615f6eb..000000000 --- a/config/decoders/decoder_file_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package decoders - -import ( - "fmt" - "github.com/stretchr/testify/require" - "io/fs" - "os" - "path/filepath" - "testing" -) - -func TestFileDecoder_Decode(t *testing.T) { - testcases := []struct { - name string - test func(t *testing.T) - }{ - { - name: "no filename set", - test: func(t *testing.T) { - s := &struct{ Name string }{} - d := NewDefaultFileDecoder() - err := d.Decode(map[string][]string{}, s) - require.NoError(t, err) - }, - }, - { - name: "file in folder mokapi in etc", - test: func(t *testing.T) { - s := &struct{ Name string }{} - f := func(path string) ([]byte, error) { - // if test is executed on windows we get second path - if path == "/etc/mokapi/mokapi.yaml" || path == "\\etc\\mokapi\\mokapi.yaml" { - return []byte("name: foobar"), nil - } - return nil, fs.ErrNotExist - } - d := &FileDecoder{readFile: f} - err := d.Decode(map[string][]string{}, s) - require.NoError(t, err) - require.Equal(t, "foobar", s.Name) - }, - }, - { - name: "file does not exist", - test: func(t *testing.T) { - s := &struct{ Name string }{} - f := func(path string) ([]byte, error) { return []byte(""), fmt.Errorf("file not found") } - d := &FileDecoder{filename: "test.yml", readFile: f} - err := d.Decode(map[string][]string{}, s) - require.Error(t, err) - }, - }, - { - name: "empty file", - test: func(t *testing.T) { - s := &struct{ Name string }{} - f := func(path string) ([]byte, error) { return []byte(""), nil } - d := &FileDecoder{filename: "mokapi.yaml", readFile: f} - err := d.Decode(map[string][]string{}, s) - require.NoError(t, err) - }, - }, - { - name: "yaml schema error", - test: func(t *testing.T) { - s := &struct{ Name int }{} - f := func(path string) ([]byte, error) { return []byte("name: {}"), nil } - d := &FileDecoder{filename: "mokapi.yml", readFile: f} - err := d.Decode(map[string][]string{}, s) - require.EqualError(t, err, "parse file 'mokapi.yml' failed: cannot unmarshal object into int") - }, - }, - { - name: "file with data", - test: func(t *testing.T) { - s := &struct{ Name string }{} - f := func(path string) ([]byte, error) { return []byte("name: foobar"), nil } - d := &FileDecoder{readFile: f} - err := d.Decode(map[string][]string{"configfile": {"mokapi.yaml"}}, s) - require.NoError(t, err) - require.Equal(t, "foobar", s.Name) - }, - }, - { - name: "temp file with data", - test: func(t *testing.T) { - s := &struct{ Name string }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "name: foobar"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configfile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, "foobar", s.Name) - }, - }, - { - name: "pascal case", - test: func(t *testing.T) { - s := &struct { - InstallDir string `yaml:"installDir"` - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "installDir: foobar"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configfile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, "foobar", s.InstallDir) - }, - }, - { - name: "map", - test: func(t *testing.T) { - s := &struct { - Key map[string]string - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "key: {foo: bar}"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configFile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, map[string]string{"foo": "bar"}, s.Key) - }, - }, - { - name: "array", - test: func(t *testing.T) { - s := &struct { - Key []string - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "key: [bar]"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configFile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, []string{"bar"}, s.Key) - }, - }, - { - name: "map with array", - test: func(t *testing.T) { - s := &struct { - Key map[string][]string - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "key: {foo: [bar]}"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configFile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, map[string][]string{"foo": {"bar"}}, s.Key) - }, - }, - { - name: "map pointer struct", - test: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]*test - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "key: {foo: {name: Bob, foo: bar}}"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configFile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - { - name: "map struct", - test: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]test - }{} - d := &FileDecoder{filename: createTempFile(t, "test.yml", "key: {foo: {name: Bob, foo: bar}}"), readFile: os.ReadFile} - err := d.Decode(map[string][]string{"configFile": {d.filename}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - tc.test(t) - }) - } -} - -func createTempFile(t *testing.T, filename, data string) string { - path := filepath.Join(t.TempDir(), filename) - file, err := os.Create(path) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - _, err = file.Write([]byte(data)) - if err != nil { - t.Fatal(err) - } - - return path -} diff --git a/config/decoders/decoder_flag.go b/config/decoders/decoder_flag.go deleted file mode 100644 index 1902d52ab..000000000 --- a/config/decoders/decoder_flag.go +++ /dev/null @@ -1,487 +0,0 @@ -package decoders - -import ( - "bytes" - "encoding/json" - "fmt" - "mokapi/config/dynamic/provider/file" - "net/url" - "reflect" - "slices" - "sort" - "strconv" - "strings" -) - -type FlagDecoder struct { - fs file.FSReader -} - -type context struct { - path string - paths []string - element reflect.Value - value []string -} - -func NewFlagDecoder() *FlagDecoder { - return &FlagDecoder{fs: &file.Reader{}} -} - -func NewFlagDecoderWithReader(reader file.FSReader) *FlagDecoder { - return &FlagDecoder{fs: reader} -} - -func (f *FlagDecoder) Decode(flags map[string][]string, element interface{}) error { - keys := make([]string, 0, len(flags)) - for k := range flags { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, name := range keys { - paths := ParsePath(name) - ctx := &context{path: name, paths: paths, value: flags[name], element: reflect.ValueOf(element)} - err := f.setValue(ctx) - if err != nil { - return fmt.Errorf("configuration error '%v' value '%v': %w", name, flags[name], err) - } - } - - return nil -} - -func (f *FlagDecoder) setValue(ctx *context) error { - switch ctx.element.Kind() { - case reflect.Struct: - if len(ctx.paths) == 0 { - return f.convert(ctx.value[0], ctx.element) - } - err := ctx.setFieldFromStruct() - if err != nil { - return f.explode(ctx.element, ctx.paths[0], ctx.value) - } - return f.setValue(ctx) - case reflect.Pointer: - if ctx.element.IsNil() { - ctx.element.Set(reflect.New(ctx.element.Type().Elem())) - } - ctx.element = ctx.element.Elem() - return f.setValue(ctx) - case reflect.String: - if len(ctx.value) > 1 { - return fmt.Errorf("expected a single string, but received multiple values") - } - s := strings.Trim(ctx.value[0], "\"") - ctx.element.SetString(s) - return nil - case reflect.Int64: - i, err := strconv.ParseInt(ctx.value[0], 10, 64) - if err != nil { - return fmt.Errorf("parse int64 failed: %v", err) - } - ctx.element.SetInt(i) - return nil - case reflect.Int: - i, err := strconv.Atoi(ctx.value[0]) - if err != nil { - return fmt.Errorf("parse int64 failed: %v", err) - } - ctx.element.SetInt(int64(i)) - return nil - case reflect.Bool: - b := false - if ctx.value[0] == "" { - b = true - } else { - var err error - b, err = strconv.ParseBool(ctx.value[0]) - if err != nil { - return fmt.Errorf("value %v cannot be parsed as bool: %v", ctx.value[0], err.Error()) - } - } - ctx.element.SetBool(b) - return nil - case reflect.Slice: - return f.setArray(ctx) - case reflect.Map: - return f.setMap(ctx) - case reflect.Interface: - if len(ctx.value) == 0 || ctx.value[0] == "" { - ctx.element.Set(reflect.ValueOf(true)) - } else { - ctx.element.Set(reflect.ValueOf(ctx.value[0])) - } - return nil - default: - return fmt.Errorf("unsupported config type: %v", ctx.element.Kind()) - } -} - -func ParsePath(key string) []string { - var paths []string - split := strings.FieldsFunc(key, func(r rune) bool { - return r == '.' || r == '-' - }) - - for _, v := range split { - if strings.HasSuffix(v, "]") { - index := strings.Index(v, "[") - paths = append(paths, v[:index], v[index:]) - } else { - paths = append(paths, v) - } - } - return paths -} - -func (f *FlagDecoder) setArray(ctx *context) error { - if len(ctx.paths) > 0 { - index, err := f.parseArrayIndex(ctx.paths[0]) - if err != nil { - return fmt.Errorf("parse array index failed: %v", err) - } - - if index >= ctx.element.Cap() { - n := index + 1 - nCap := 2 * n - if nCap < 4 { - nCap = 4 - } - if ctx.element.IsNil() { - s := reflect.MakeSlice(ctx.element.Type(), n, nCap) - ctx.element.Set(s) - } else { - s := reflect.MakeSlice(ctx.element.Type(), n, nCap) - reflect.Copy(s, ctx.element) - ctx.element.Set(s) - } - } - if index >= ctx.element.Len() { - ctx.element.SetLen(index + 1) - } - - return f.setValue(ctx.Next(ctx.element.Index(index))) - } else { - if len(ctx.value) == 1 { - ctx.value = splitArrayItems(ctx.value[0]) - } - - if len(ctx.value) > 1 { - // reset slice; remove default values - ctx.element.Set(reflect.MakeSlice(ctx.element.Type(), 0, len(ctx.value))) - } - - for _, v := range ctx.value { - ptr := reflect.New(ctx.element.Type().Elem()) - ctxItem := &context{ - paths: ctx.paths, - element: ptr, - value: []string{v}, - } - if err := f.setValue(ctxItem); err != nil { - return err - } - ctx.element.Set(reflect.Append(ctx.element, ptr.Elem())) - } - } - - return nil -} - -func (f *FlagDecoder) parseArrayIndex(path string) (int, error) { - if strings.HasPrefix(path, "[") { - s := strings.TrimPrefix(path, "[") - s = strings.TrimSuffix(s, "]") - return strconv.Atoi(s) - - } - return strconv.Atoi(path) -} - -func (f *FlagDecoder) setMap(ctx *context) error { - m := ctx.element - if m.IsNil() { - m.Set(reflect.MakeMap(ctx.element.Type())) - } - - i := m.Interface() - _ = i - - var key reflect.Value - if len(ctx.paths) >= 1 { - key = reflect.ValueOf(ctx.paths[0]) - } else if len(ctx.paths) == 0 { - if len(ctx.value) == 1 { - kv := strings.Split(ctx.value[0], "=") - if len(kv) != 2 { - return fmt.Errorf("expected value with key value pair for map like key=value: %s", ctx.value[0]) - } - key = reflect.ValueOf(kv[0]) - ctx.value = []string{kv[1]} - } - } - - if !key.IsValid() { - return fmt.Errorf("expected key to set map value") - } - - v := m.MapIndex(key) - if !v.IsValid() { - v = reflect.New(m.Type().Elem()) - } else { - p := reflect.New(m.Type().Elem()) - p.Elem().Set(v) - v = p - } - - ctx.element = v - - if len(ctx.paths) >= 1 { - v = ctx.element - if err := f.setValue(ctx.Next(ctx.element)); err != nil { - return err - } - } else { - if err := f.setValue(ctx); err != nil { - return err - } - } - - m.SetMapIndex(key, v.Elem()) - - return nil -} - -func (f *FlagDecoder) explode(v reflect.Value, name string, value []string) error { - field := getFieldByTag(v, name, "explode") - if !field.IsValid() { - return fmt.Errorf("not found") - } - - for _, val := range value { - o := reflect.New(field.Type().Elem()) - err := f.convert(val, o.Elem()) - if err != nil { - return err - } - field.Set(reflect.Append(field, o.Elem())) - } - - return nil -} - -func getFieldByTag(structValue reflect.Value, name, tag string) reflect.Value { - for i := 0; i < structValue.NumField(); i++ { - v := structValue.Type().Field(i).Tag.Get(tag) - tagValues := strings.Split(v, ",") - for _, tagValue := range tagValues { - if tagValue == name { - return structValue.Field(i) - } - } - } - return reflect.Value{} -} - -func (f *FlagDecoder) convert(s string, v reflect.Value) error { - u, err := url.ParseRequestURI(s) - if err == nil { - switch u.Scheme { - case "file": - var path string - if len(u.Host) > 0 { - path = u.Host - } - if len(u.Path) > 0 { - path += u.Path - } - if len(u.Opaque) > 0 { - path = u.Opaque - } - b, err := f.fs.ReadFile(path) - if err != nil { - return err - } - // remove bom sequence if present - if len(b) >= 4 && bytes.Equal(b[0:3], file.Bom) { - b = b[3:] - } - s = string(b) - } - } - - kind := v.Type().Kind() - if kind == reflect.Struct { - err = f.convertJson(s, v) - if err == nil { - return nil - } - } - - if kind == reflect.Struct { - pairs := strings.Split(s, ",") - for _, pair := range pairs { - kv := strings.Split(pair, "=") - if len(kv) != 2 { - return fmt.Errorf("parse shorthand failed: %v", s) - } - err = f.setValue(&context{paths: []string{kv[0]}, value: []string{kv[1]}, element: v}) - if err != nil { - return err - } - } - return nil - } else if kind == reflect.Slice { - return f.setValue(&context{paths: []string{}, element: v, value: []string{s}}) - } else if kind == reflect.String { - v.Set(reflect.ValueOf(s)) - return nil - } - - return fmt.Errorf("not supported") -} - -func (f *FlagDecoder) convertJson(s string, v reflect.Value) error { - m := map[string]interface{}{} - err := json.Unmarshal([]byte(s), &m) - if err != nil { - return err - } - - return f.setJson(v, m) -} - -func splitArrayItems(s string) []string { - quoted := false - return strings.FieldsFunc(s, func(r rune) bool { - if r == '"' { - quoted = !quoted - } - return !quoted && r == ' ' - }) -} - -func (f *FlagDecoder) setJson(element reflect.Value, i interface{}) error { - switch o := i.(type) { - case float64: - // currently, config uses only int64 as number - i = int64(o) - element.Set(reflect.ValueOf(i)) - case int64, string, bool: - element.Set(reflect.ValueOf(i)) - case []interface{}: - for _, item := range o { - ptr := reflect.New(element.Type().Elem()) - err := f.setJson(ptr.Elem(), item) - if err != nil { - return err - } - element.Set(reflect.Append(element, ptr.Elem())) - } - case map[string]interface{}: - for k, v := range o { - field := element.FieldByNameFunc(func(f string) bool { return strings.ToLower(f) == strings.ToLower(k) }) - if field.IsValid() { - err := f.setJson(field, v) - if err != nil { - return err - } - } else { - field = getFieldByTag(element, k, "explode") - if !field.IsValid() { - return fmt.Errorf("configuration not found") - } - ptr := reflect.New(field.Type().Elem()) - err := f.setJson(ptr.Elem(), v) - if err != nil { - return err - } - field.Set(reflect.Append(field, ptr.Elem())) - } - } - } - - return nil -} - -func invertFlag(value string) (string, error) { - flag := false - if value != "" { - b, err := strconv.ParseBool(value) - if err != nil { - return "", fmt.Errorf("value %v cannot be parsed as bool: %v", value, err.Error()) - } - flag = !b - } - return fmt.Sprintf("%v", flag), nil -} - -func (c *context) setFieldFromStruct() error { - name := strings.ToLower(c.paths[0]) - field := c.element.FieldByNameFunc(func(f string) bool { - return strings.ToLower(f) == name - }) - if field.IsValid() { - c.Next(field) - return nil - } - - if c.paths[0] == "no" && len(c.paths) == 2 { - name = strings.ToLower(c.paths[1]) - field = c.element.FieldByNameFunc(func(f string) bool { - return strings.ToLower(f) == c.paths[1] - }) - if field.IsValid() { - value, err := invertFlag(c.value[0]) - if err != nil { - return err - } - c.value = []string{value} - c.Next(field) - return nil - } - } - - for i := 0; i < c.element.NumField(); i++ { - f := c.element.Type().Field(i) - - var names []string - tag := f.Tag.Get("flag") - if len(tag) == 0 { - tag = f.Tag.Get("name") - if len(tag) > 0 { - names = append(names, tag) - } else { - tag = f.Tag.Get("aliases") - if len(tag) > 0 { - names = append(names, strings.Split(tag, ",")...) - } else { - continue - } - } - } else { - names = append(names, tag) - } - - name = "" - for j := 0; j < len(c.paths); j++ { - if len(name) > 0 { - name += "-" - } - name += c.paths[j] - if slices.Contains(names, name) { - c.element = c.element.Field(i) - c.paths = c.paths[j+1:] - return nil - } - } - } - return fmt.Errorf("no configuration found") -} - -func (c *context) Next(element reflect.Value) *context { - c.paths = c.paths[1:] - c.element = element - return c -} diff --git a/config/decoders/decoder_flag_test.go b/config/decoders/decoder_flag_test.go deleted file mode 100644 index 99b5cad03..000000000 --- a/config/decoders/decoder_flag_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package decoders - -import ( - "github.com/stretchr/testify/require" - "mokapi/config/dynamic/provider/file/filetest" - "testing" -) - -func TestFlagDecoder_Decode(t *testing.T) { - testcases := []struct { - name string - f func(t *testing.T) - }{ - { - name: "string", - f: func(t *testing.T) { - s := &struct { - Name string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"name": {"foobar"}}, s) - require.NoError(t, err) - require.Equal(t, "foobar", s.Name) - }, - }, - { - name: "config is string but multiple argument values", - f: func(t *testing.T) { - s := &struct { - Name string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"name": {"foo", "bar"}}, s) - require.EqualError(t, err, "configuration error 'name' value '[foo bar]': expected a single string, but received multiple values") - }, - }, - { - name: "bool", - f: func(t *testing.T) { - s := &struct { - Flag1 bool - Flag2 bool - Flag3 bool - Flag4 bool - }{ - Flag3: true, - Flag4: true, - } - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"flag1": {"true"}, "flag2": {"1"}, "no-flag3": {""}, "no-flag4": {"false"}}, s) - require.NoError(t, err) - require.True(t, s.Flag1) - require.True(t, s.Flag2) - require.False(t, s.Flag3) - require.True(t, s.Flag4) - }, - }, - { - name: "not a bool", - f: func(t *testing.T) { - s := &struct { - Flag1 bool - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"flag1": {"foo"}}, s) - require.EqualError(t, err, "configuration error 'flag1' value '[foo]': value foo cannot be parsed as bool: strconv.ParseBool: parsing \"foo\": invalid syntax") - require.False(t, s.Flag1) - }, - }, - { - name: "nested with dot (old)", - f: func(t *testing.T) { - s := &struct { - Key string - Value struct { - Flag bool - } - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"key": {"foo"}, "value.flag": {"true"}}, s) - require.NoError(t, err) - require.Equal(t, "foo", s.Key) - require.True(t, s.Value.Flag) - }, - }, - { - name: "nested with - (new)", - f: func(t *testing.T) { - s := &struct { - Key string - Value struct { - Flag bool - } - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"key": {"foo"}, "value-flag": {"true"}}, s) - require.NoError(t, err) - require.Equal(t, "foo", s.Key) - require.True(t, s.Value.Flag) - }, - }, - { - name: "capitalized", - f: func(t *testing.T) { - s := &struct { - Key string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key": {"foo"}}, s) - require.NoError(t, err) - require.Equal(t, "foo", s.Key) - }, - }, - { - name: "map", - f: func(t *testing.T) { - s := &struct { - Key map[string]string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key.foo": {"bar"}}, s) - require.NoError(t, err) - require.Equal(t, map[string]string{"foo": "bar"}, s.Key) - }, - }, - { - name: "array", - f: func(t *testing.T) { - s := &struct { - Key []string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key[0]": {"bar"}, "Key[1]": {"foo"}}, s) - require.NoError(t, err) - require.Equal(t, []string{"bar", "foo"}, s.Key) - }, - }, - { - name: "array shorthand", - f: func(t *testing.T) { - s := &struct { - Key []string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key": {"bar foo"}}, s) - require.NoError(t, err) - require.Equal(t, []string{"bar", "foo"}, s.Key) - }, - }, - { - name: "array shorthand with item contains a space", - f: func(t *testing.T) { - s := &struct { - Key []string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key": {"bar foo \"foo bar\""}}, s) - require.NoError(t, err) - require.Equal(t, []string{"bar", "foo", "foo bar"}, s.Key) - }, - }, - { - name: "map with array", - f: func(t *testing.T) { - s := &struct { - Key map[string][]string - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key.foo[0]": {"bar"}}, s) - require.NoError(t, err) - require.Equal(t, map[string][]string{"foo": {"bar"}}, s.Key) - }, - }, - { - name: "map pointer struct", - f: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]*test - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key.foo.Name": {"Bob"}, "Key.foo.Foo": {"bar"}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - { - name: "map struct", - f: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]test - }{} - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"Key.foo.Name": {"Bob"}, "Key.foo.Foo": {"bar"}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - { - name: "parameters from file in current directory", - f: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]test - }{} - - fs := &filetest.MockFS{Entries: []*filetest.Entry{ - { - Name: "test.json", - IsDir: false, - Data: []byte(`{"name": "Bob", "foo": "bar"}`), - }}} - - d := &FlagDecoder{fs: fs} - err := d.Decode(map[string][]string{"Key.foo": {"file://test.json"}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - { - name: "parameters from file absolute path", - f: func(t *testing.T) { - type test struct { - Name string - Foo string - } - s := &struct { - Key map[string]test - }{} - - fs := &filetest.MockFS{Entries: []*filetest.Entry{ - { - Name: "/tmp/test.json", - IsDir: false, - Data: []byte(`{"name": "Bob", "foo": "bar"}`), - }}} - - d := &FlagDecoder{fs: fs} - err := d.Decode(map[string][]string{"Key.foo": {"file:///tmp/test.json"}}, s) - require.NoError(t, err) - require.Equal(t, "Bob", s.Key["foo"].Name) - require.Equal(t, "bar", s.Key["foo"].Foo) - }, - }, - { - name: "parameters from file absolute path", - f: func(t *testing.T) { - s := &struct { - SkipName string `flag:"skip-name"` - }{} - - d := &FlagDecoder{} - err := d.Decode(map[string][]string{"skip-name": {"foo"}}, s) - require.NoError(t, err) - require.Equal(t, "foo", s.SkipName) - }, - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - tc.f(t) - }) - } -} diff --git a/config/decoders/decoder_test.go b/config/decoders/decoder_test.go deleted file mode 100644 index ade18aa2a..000000000 --- a/config/decoders/decoder_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package decoders - -import ( - log "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - "os" - "strings" - "testing" -) - -func TestLoad(t *testing.T) { - testcases := []struct { - name string - f func(t *testing.T) - }{ - { - name: "args", - f: func(t *testing.T) { - s := &struct { - Name string - Flag1 bool - Flag2 bool - }{ - Flag2: true, - } - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name=bar") - os.Args = append(os.Args, "--flag1") - os.Args = append(os.Args, "--no-flag2") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, "bar", s.Name) - require.Equal(t, true, s.Flag1) - require.Equal(t, false, s.Flag2) - }, - }, - { - name: "without =", - f: func(t *testing.T) { - s := &struct { - Name string - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name") - os.Args = append(os.Args, "bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, "bar", s.Name) - }, - }, - { - name: "array explode", - f: func(t *testing.T) { - s := &struct { - Names []string `explode:"name"` - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name", "foo", "--name", "bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, []string{"foo", "bar"}, s.Names) - }, - }, - { - name: "array list", - f: func(t *testing.T) { - s := &struct { - Names []string `explode:"name"` - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name", "foo", "bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, []string{"foo", "bar"}, s.Names) - }, - }, - { - name: "array with item contains a space", - f: func(t *testing.T) { - s := &struct { - Names []string - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--names") - os.Args = append(os.Args, "bar", "foo", "foo bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, []string{"bar", "foo", "foo bar"}, s.Names) - }, - }, - { - name: "array override with index operator", - f: func(t *testing.T) { - s := &struct { - Names []string `explode:"name"` - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name", "foo", "--name", "bar", "--names[0]", "x") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, []string{"x", "bar"}, s.Names) - }, - }, - { - name: "env var", - f: func(t *testing.T) { - s := &struct { - Name string - SkipName string `flag:"skip-name"` - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_name", "bar") - defer os.Unsetenv("MOKAPI_name") - require.NoError(t, err) - err = os.Setenv("MOKAPI_SKIP_NAME", "bar") - defer os.Unsetenv("MOKAPI_SKIP_NAME") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, "bar", s.Name) - require.Equal(t, "bar", s.SkipName) - }, - }, - { - name: "env var overrides cli args", - f: func(t *testing.T) { - s := &struct { - Name string - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name=bar") - err := os.Setenv("MOKAPI_name", "barr") - defer os.Unsetenv("MOKAPI_name") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, "barr", s.Name) - }, - }, - { - name: "env var overrides cli array", - f: func(t *testing.T) { - s := &struct { - Name []string - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--name", "foo", "--name", "bar") - err := os.Setenv("MOKAPI_name", "barr") - defer os.Unsetenv("MOKAPI_name") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, []string{"barr"}, s.Name) - }, - }, - { - name: "env var array single", - f: func(t *testing.T) { - s := &struct { - Urls []string - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_urls", "https://foo.bar") - defer os.Unsetenv("MOKAPI_urls") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Contains(t, s.Urls, "https://foo.bar") - require.Equal(t, 1, len(s.Urls)) - require.Equal(t, 1, cap(s.Urls)) - }, - }, - { - name: "env var array", - f: func(t *testing.T) { - s := &struct { - Urls []string - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_urls[0]", "https://foo.bar") - defer os.Unsetenv("MOKAPI_urls[0]") - require.NoError(t, err) - err = os.Setenv("MOKAPI_urls[1]", "https://mokapi.io") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_urls[1]") - err = os.Setenv("MOKAPI_urls_2", "https://foo.com") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_urls_2") - err = os.Setenv("MOKAPI_urls.10", "https://bar.com") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_urls.10") - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Contains(t, s.Urls, "https://foo.bar") - require.Contains(t, s.Urls, "https://mokapi.io") - require.Contains(t, s.Urls, "https://foo.com") - require.Contains(t, s.Urls, "https://bar.com") - require.Equal(t, 11, len(s.Urls)) - require.Equal(t, 22, cap(s.Urls)) - }, - }, - { - name: "env var array update index with [0]", - f: func(t *testing.T) { - s := &struct { - Items []struct { - Name string - Value int64 - } - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_items[0].name", "mokapi") - defer os.Unsetenv("MOKAPI_items[0].name") - require.NoError(t, err) - err = os.Setenv("MOKAPI_items[0].value", "123") - defer os.Unsetenv("MOKAPI_items[0].value") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Len(t, s.Items, 1) - require.Equal(t, s.Items[0].Name, "mokapi") - require.Equal(t, s.Items[0].Value, int64(123)) - }, - }, - { - name: "env var array update index with _0_", - f: func(t *testing.T) { - s := &struct { - Items []struct { - Name string - Value int64 - } - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_items_0_name", "mokapi") - defer os.Unsetenv("MOKAPI_items_0_name") - require.NoError(t, err) - err = os.Setenv("MOKAPI_items_0_value", "123") - defer os.Unsetenv("MOKAPI_items_0_value") - require.NoError(t, err) - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Len(t, s.Items, 1) - require.Equal(t, s.Items[0].Name, "mokapi") - require.Equal(t, s.Items[0].Value, int64(123)) - }, - }, - { - name: "env var array update index with .0.", - f: func(t *testing.T) { - s := &struct { - Items []struct { - Name string - Value int64 - } - }{} - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_items_0_name", "mokapi") - defer os.Unsetenv("MOKAPI_items_0_name") - require.NoError(t, err) - err = os.Setenv("MOKAPI_ITEMS_0_VALUE", "123") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_ITEMS_0_VALUE") - err = os.Setenv("MOKAPI_items.0.value", "123") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_items.0.value") - - err = Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Len(t, s.Items, 1) - require.Equal(t, s.Items[0].Name, "mokapi") - require.Equal(t, s.Items[0].Value, int64(123)) - }, - }, - { - name: "unknown argument", - f: func(t *testing.T) { - s := &struct { - Name string - }{} - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--foo=bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.EqualError(t, err, "configuration error 'foo' value '[bar]': not found") - }, - }, - { - name: "argument after positional argument", - f: func(t *testing.T) { - s := &struct { - Foo string - Args []string `json:"args" yaml:"-" aliases:"args"` - }{} - os.Args = append(os.Args, "mokapi.exe", "file.json", "--foo=bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.NoError(t, err) - require.Equal(t, s.Foo, "bar") - require.Equal(t, s.Args, []string{"file.json"}) - }, - }, - { - name: "after -- an additional flag argument", - f: func(t *testing.T) { - s := &struct { - Foo string - Args []string `json:"args" yaml:"-" aliases:"args"` - }{} - os.Args = append(os.Args, "mokapi.exe", "--", "--foo=bar") - - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.EqualError(t, err, "unknown positional argument: '--foo=bar'") - }, - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - log.Infof("Test: %s", tc.name) - os.Args = nil - log.Infof("Args: %s", os.Args) - tc.f(t) - }) - } -} - -func TestLoad_Invalid(t *testing.T) { - testcases := []struct { - args string - }{ - {args: "name=bar"}, - {args: "-"}, - {args: "-=bar"}, - {args: "---name=bar"}, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.args, func(t *testing.T) { - os.Args = nil - os.Args = append(os.Args, "mokapi.exe") - args := strings.Split(tc.args, " ") - os.Args = append(os.Args, args...) - s := &struct { - Name string - }{} - err := Load([]ConfigDecoder{&FlagDecoder{}}, s) - require.Error(t, err) - }) - } -} diff --git a/config/decoders/parser.go b/config/decoders/parser.go deleted file mode 100644 index 9083784d4..000000000 --- a/config/decoders/parser.go +++ /dev/null @@ -1,105 +0,0 @@ -package decoders - -import ( - "fmt" - "os" - "strings" -) - -const DefaultEnvNamePrefix = "MOKAPI_" - -func parseFlags() (map[string][]string, error) { - flags, err := parseArgs(os.Args[1:]) // first argument is the program path - if err != nil { - return nil, err - } - - envs := parseEnv(os.Environ()) - - // merge maps. env flags overwrites cli flags - for k, v := range envs { - flags[k] = []string{v} - } - - return flags, nil -} - -func parseEnv(environ []string) map[string]string { - dictionary := make(map[string]string) - - for _, s := range environ { - kv := strings.SplitN(s, "=", 2) - if strings.HasPrefix(strings.ToUpper(kv[0]), DefaultEnvNamePrefix) { - key := strings.Replace(kv[0], DefaultEnvNamePrefix, "", 1) - name := strings.ReplaceAll(strings.ToLower(key), "_", "-") - dictionary[name] = kv[1] - } - } - - return dictionary -} - -func parseArgs(args []string) (map[string][]string, error) { - dictionary := make(map[string][]string) - inPositionalArgs := false - for i := 0; i < len(args); i++ { - s := args[i] - if len(s) < 2 || s[0] != '-' { - dictionary["args"] = append(dictionary["args"], s) - continue - } else if inPositionalArgs { - // currently, no positional argument with prefix -- are defined - return nil, fmt.Errorf("unknown positional argument: '%s'", s) - } - - index := 1 - - if s[1] == '-' { - index++ - if len(s) == 2 { - inPositionalArgs = true - continue - } - } - - name := s[index:] - value := "" - hasValue := false - - if len(name) == 0 || name[0] == '-' || name[0] == '=' { - return nil, fmt.Errorf("invalid argument %v", s) - } - - // search for = - for i := 1; i < len(name); i++ { // = can not be first - if name[i] == '=' { - value = name[i+1:] - name = name[0:i] - hasValue = true - break - } - } - - param := strings.ToLower(name) - if hasValue { - dictionary[param] = append(dictionary[param], value) - continue - } - - // value is next args - for i++; i < len(args); i++ { - if strings.HasPrefix(args[i], "--") { - i-- - break - } - value = args[i] - dictionary[param] = append(dictionary[param], value) - } - - if len(dictionary[param]) == 0 { - dictionary[param] = append(dictionary[param], "") - } - } - - return dictionary, nil -} diff --git a/config/dynamic/config_info.go b/config/dynamic/config_info.go index ace6da02d..113a3ca0e 100644 --- a/config/dynamic/config_info.go +++ b/config/dynamic/config_info.go @@ -3,11 +3,12 @@ package dynamic import ( "crypto/md5" "encoding/hex" - "github.com/google/uuid" - log "github.com/sirupsen/logrus" "net/url" "strings" "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" ) type ConfigInfo struct { @@ -16,6 +17,7 @@ type ConfigInfo struct { Checksum []byte Time time.Time inner *ConfigInfo + Tags []string } func (ci *ConfigInfo) Path() string { diff --git a/config/dynamic/mail/parse.go b/config/dynamic/mail/parse.go index e64ff64cb..7cf9aa5b9 100644 --- a/config/dynamic/mail/parse.go +++ b/config/dynamic/mail/parse.go @@ -10,7 +10,7 @@ func (c *Config) Parse(config *dynamic.Config, _ dynamic.Reader) error { return nil } - log.Warnf("Deprecated mail configuration in %s. This format is deprecated and will be removed in future versions. Please migrate to the new format. More info: https://mokapi.io/docs/guides/email", config.Info.Path()) + log.Warnf("Deprecated mail configuration in %s. This format is deprecated and will be removed in future versions. Please migrate to the new format. More info: https://mokapi.io/docs/email/overview", config.Info.Path()) converted := c.Convert() config.Data = converted diff --git a/config/dynamic/mail/parse_test.go b/config/dynamic/mail/parse_test.go index c8a9b7713..eaca9db9e 100644 --- a/config/dynamic/mail/parse_test.go +++ b/config/dynamic/mail/parse_test.go @@ -33,5 +33,5 @@ func TestConfig_Parse(t *testing.T) { require.Len(t, hook.Entries, 1) require.Equal(t, logrus.WarnLevel, hook.Entries[0].Level) - require.Equal(t, "Deprecated mail configuration in file://foo.yml. This format is deprecated and will be removed in future versions. Please migrate to the new format. More info: https://mokapi.io/docs/guides/email", hook.Entries[0].Message) + require.Equal(t, "Deprecated mail configuration in file://foo.yml. This format is deprecated and will be removed in future versions. Please migrate to the new format. More info: https://mokapi.io/docs/email/overview", hook.Entries[0].Message) } diff --git a/config/dynamic/provider/file/file.go b/config/dynamic/provider/file/file.go index a034af260..0cd8cac8e 100644 --- a/config/dynamic/provider/file/file.go +++ b/config/dynamic/provider/file/file.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "errors" "fmt" "io" "io/fs" @@ -92,20 +93,26 @@ func (p *Provider) Read(u *url.URL) (*dynamic.Config, error) { func (p *Provider) Start(ch chan dynamic.ConfigEvent, pool *safe.Pool) error { p.ch = ch - var path []string + var files []static.FileConfig if len(p.cfg.Directories) > 0 { - path = p.cfg.Directories + for _, dir := range p.cfg.Directories { + for _, d := range strings.Split(dir.Path, string(os.PathListSeparator)) { + files = append(files, static.FileConfig{Path: d, Include: dir.Include, Exclude: dir.Exclude}) + } + } } else if len(p.cfg.Filenames) > 0 { - path = p.cfg.Filenames + for _, file := range p.cfg.Filenames { + for _, f := range strings.Split(file, string(os.PathListSeparator)) { + files = append(files, static.FileConfig{Path: f}) + } + } } - if len(path) > 0 { + if len(files) > 0 { pool.Go(func(ctx context.Context) { - for _, pathItem := range path { - for _, i := range strings.Split(pathItem, string(os.PathListSeparator)) { - if err := p.walk(i); err != nil { - log.Errorf("file provider: %v", err) - } + for _, file := range files { + if err := p.walk(file); err != nil { + log.Errorf("file provider: %v", err) } } p.isInit = false @@ -118,7 +125,7 @@ func (p *Provider) Start(ch chan dynamic.ConfigEvent, pool *safe.Pool) error { func (p *Provider) Watch(dir string, pool *safe.Pool) { pool.Go(func(ctx context.Context) { - if err := p.walk(dir); err != nil { + if err := p.walk(static.FileConfig{Path: dir}); err != nil { log.Errorf("file provider: %v", err) } }) @@ -170,13 +177,30 @@ func (p *Provider) watch(pool *safe.Pool) error { return nil } -func (p *Provider) skip(path string, isDir bool) bool { +func (p *Provider) skip(path string, isDir bool, info static.FileConfig) bool { if p.isWatchPath(path) { return false } - if !isDir && len(p.cfg.Include) > 0 { - return !include(p.cfg.Include, path) + if !isDir { + inc := p.cfg.Include + if len(info.Include) > 0 { + inc = append(inc, info.Include...) + } + if len(inc) > 0 { + if !include(inc, path) { + return true + } + } + ex := p.cfg.Exclude + if len(info.Exclude) > 0 { + ex = append(ex, info.Exclude...) + } + if len(ex) > 0 { + if include(ex, path) { + return true + } + } } if isMokapiIgnoreFile(path) { @@ -227,20 +251,20 @@ func (p *Provider) readFile(path string) (*dynamic.Config, error) { }, nil } -func (p *Provider) walk(root string) error { - p.readMokapiIgnore(root) +func (p *Provider) walk(fileInfo static.FileConfig) error { + p.readMokapiIgnore(fileInfo.Path) walkDir := func(path string, fi fs.DirEntry, err error) error { if err != nil { return err } if fi.IsDir() { - if p.skip(path, true) && path != root { + if p.skip(path, true, fileInfo) && path != fileInfo.Path { log.Debugf("skip dir: %v", path) return filepath.SkipDir } p.readMokapiIgnore(path) p.watchPath(path) - } else if !p.skip(path, false) { + } else if !p.skip(path, false, fileInfo) { if c, err := p.readFile(path); err != nil { log.Error(err) } else if len(c.Raw) > 0 { @@ -254,7 +278,7 @@ func (p *Provider) walk(root string) error { return nil } - return p.fs.Walk(root, walkDir) + return p.fs.Walk(fileInfo.Path, walkDir) } func (p *Provider) readMokapiIgnore(path string) { @@ -345,10 +369,12 @@ func (p *Provider) processEvents(events []fsnotify.Event) { } dir, _ := filepath.Split(evt.Name) - if dir == evt.Name && !p.skip(dir, true) { - p.watchPath(dir) - } else { - if !p.skip(evt.Name, false) { + isDir := dir == evt.Name + + if !p.skipEvent(e, isDir) { + if isDir { + p.watchPath(dir) + } else { e.Config, err = p.readFile(evt.Name) if err != nil { log.Errorf("unable to read file %v", evt.Name) @@ -371,13 +397,31 @@ Walk: } doneWalk = append(doneWalk, dir) - err := p.walk(dir) + cfg, err := p.getFileConfig(dir) + if err != nil { + log.Debugf("skip event: unable to get file config for %v: %v", dir, err) + } + + cfg.Path = dir + err = p.walk(cfg) if err != nil { log.Errorf("unable to process dir %v: %v", dir, err) } } } +func (p *Provider) skipEvent(evt dynamic.ConfigEvent, isDir bool) bool { + if p.isWatchPath(evt.Name) { + return false + } + cfg, err := p.getFileConfig(evt.Name) + if err != nil { + log.Debugf("skip event: unable to get file config for %v: %v", evt.Name, err) + } + + return p.skip(evt.Name, isDir, cfg) +} + func (p *Provider) isWatchPath(path string) bool { p.m.Lock() defer p.m.Unlock() @@ -386,6 +430,15 @@ func (p *Provider) isWatchPath(path string) bool { return ok } +func (p *Provider) getFileConfig(path string) (static.FileConfig, error) { + for _, cfg := range p.cfg.Directories { + if isSub(cfg.Path, path) { + return cfg, nil + } + } + return static.FileConfig{}, errors.New("directory config not found") +} + func include(s []string, v string) bool { for _, i := range s { if Match(i, v) { @@ -399,3 +452,15 @@ func isMokapiIgnoreFile(path string) bool { name := filepath.Base(path) return name == mokapiIgnoreFile } + +func isSub(parent, sub string) bool { + up := ".." + string(os.PathSeparator) + rel, err := filepath.Rel(parent, sub) + if err != nil { + return false + } + if !strings.HasPrefix(rel, up) && rel != ".." { + return true + } + return false +} diff --git a/config/dynamic/provider/file/file_test.go b/config/dynamic/provider/file/file_test.go index 8334b7611..418e3408d 100644 --- a/config/dynamic/provider/file/file_test.go +++ b/config/dynamic/provider/file/file_test.go @@ -2,7 +2,6 @@ package file import ( "context" - "github.com/stretchr/testify/require" "io" "mokapi/config/dynamic" "mokapi/config/dynamic/provider/file/filetest" @@ -15,6 +14,8 @@ import ( "strings" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestProvider(t *testing.T) { @@ -40,7 +41,7 @@ func TestProvider(t *testing.T) { IsDir: false, Data: []byte("foobar"), }}}, - cfg: static.FileProvider{Directories: []string{"./foo"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./foo"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -54,7 +55,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), ModTime: mustTime("2024-01-02T15:04:05Z"), }}}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "foo.txt", filepath.Base(events[0].Config.Info.Path())) @@ -70,7 +71,7 @@ func TestProvider(t *testing.T) { IsDir: false, Data: []byte("fo"), }}}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "foo.txt", filepath.Base(events[0].Config.Info.Path())) @@ -85,7 +86,7 @@ func TestProvider(t *testing.T) { IsDir: false, Data: []byte{0xEF, 0xBB, 0xBF, 'f', 'o', 'o', 'b', 'a', 'r'}, }}}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "foo.txt", filepath.Base(events[0].Config.Info.Path())) @@ -100,7 +101,7 @@ func TestProvider(t *testing.T) { IsDir: false, Data: []byte("foobar"), }}}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -119,7 +120,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}, SkipPrefix: []string{"$"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}, SkipPrefix: []string{"$"}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "_foo.txt", filepath.Base(events[0].Config.Info.Path())) @@ -138,7 +139,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "foo.txt", filepath.Base(events[0].Config.Info.Path())) @@ -157,7 +158,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -176,7 +177,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -199,7 +200,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -227,7 +228,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.Equal(t, "foobar", string(events[0].Config.Raw)) @@ -247,7 +248,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) }, @@ -266,7 +267,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -290,7 +291,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) }, @@ -319,7 +320,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) require.True(t, strings.HasSuffix(events[0].Config.Info.Path(), filepath.Join("dir", "foo.js"))) @@ -354,7 +355,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 2) require.True(t, strings.HasSuffix(events[0].Config.Info.Path(), filepath.Join("dir", "foo.js"))) @@ -390,7 +391,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 2) sort.Slice(events, func(i, j int) bool { @@ -414,7 +415,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 0) }, @@ -433,7 +434,7 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}, Include: []string{"foo"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}, Include: []string{"foo"}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 1) }, @@ -477,11 +478,97 @@ func TestProvider(t *testing.T) { Data: []byte("foobar"), }, }}, - cfg: static.FileProvider{Directories: []string{"./"}, Include: []string{"*.js"}}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./"}}, Include: []string{"*.js"}}, test: func(t *testing.T, events []dynamic.ConfigEvent) { require.Len(t, events, 4) }, }, + { + name: "include index.js", + fs: &filetest.MockFS{Entries: []*filetest.Entry{ + { + Name: "/bar.txt", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/index.js", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/foo.js", + IsDir: false, + Data: []byte("foobar"), + }, + }}, + cfg: static.FileProvider{Directories: []static.FileConfig{{Path: "./", Include: []string{"*.js"}}}}, + test: func(t *testing.T, events []dynamic.ConfigEvent) { + require.Len(t, events, 2) + require.Equal(t, "dir/index.js", events[0].Name) + require.Equal(t, "dir/foo.js", events[1].Name) + }, + }, + { + name: "include and exclude", + fs: &filetest.MockFS{Entries: []*filetest.Entry{ + { + Name: "/bar.txt", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/index.js", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/foo.js", + IsDir: false, + Data: []byte("foobar"), + }, + }}, + cfg: static.FileProvider{Directories: []static.FileConfig{{ + Path: "./", + Include: []string{"*.js"}, + Exclude: []string{"*foo.js"}, + }}}, + test: func(t *testing.T, events []dynamic.ConfigEvent) { + require.Len(t, events, 1) + require.Equal(t, "dir/index.js", events[0].Name) + }, + }, + { + name: "extend include", + fs: &filetest.MockFS{Entries: []*filetest.Entry{ + { + Name: "/bar.txt", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/index.js", + IsDir: false, + Data: []byte("foobar"), + }, + { + Name: "dir/foo.ts", + IsDir: false, + Data: []byte("foobar"), + }, + }}, + cfg: static.FileProvider{ + Include: []string{"*.js"}, + Directories: []static.FileConfig{{ + Path: "./", + Include: []string{"*.ts"}, + }}}, + test: func(t *testing.T, events []dynamic.ConfigEvent) { + require.Len(t, events, 2) + require.Equal(t, "dir/index.js", events[0].Name) + require.Equal(t, "dir/foo.ts", events[1].Name) + }, + }, } t.Parallel() @@ -595,8 +682,8 @@ func TestProvider_File(t *testing.T) { func TestWatch_AddFile(t *testing.T) { ch := make(chan dynamic.ConfigEvent) tempDir := t.TempDir() - t.Cleanup(func() { os.RemoveAll(tempDir) }) - p := New(static.FileProvider{Directories: []string{tempDir}}) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) + p := New(static.FileProvider{Directories: []static.FileConfig{{Path: tempDir}}}) pool := safe.NewPool(context.Background()) defer pool.Stop() @@ -604,7 +691,7 @@ func TestWatch_AddFile(t *testing.T) { require.NoError(t, err) time.Sleep(500 * time.Millisecond) - err = createTempFile("./test/openapi.yml", p.cfg.Directories[0]) + err = createTempFile("./test/openapi.yml", p.cfg.Directories[0].Path) require.NoError(t, err) timeout := time.After(5 * time.Second) @@ -620,8 +707,8 @@ func TestWatch_AddFile(t *testing.T) { func TestWatch_Create_SubFolder_And_Add_File(t *testing.T) { ch := make(chan dynamic.ConfigEvent) tempDir := t.TempDir() - t.Cleanup(func() { os.RemoveAll(tempDir) }) - p := New(static.FileProvider{Directories: []string{tempDir}}) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) + p := New(static.FileProvider{Directories: []static.FileConfig{{Path: tempDir}}}) pool := safe.NewPool(context.Background()) defer pool.Stop() @@ -629,7 +716,7 @@ func TestWatch_Create_SubFolder_And_Add_File(t *testing.T) { require.NoError(t, err) time.Sleep(500 * time.Millisecond) - err = createTempFile("./test/openapi.yml", filepath.Join(p.cfg.Directories[0], "foo")) + err = createTempFile("./test/openapi.yml", filepath.Join(p.cfg.Directories[0].Path, "foo")) require.NoError(t, err) timeout := time.After(5 * time.Second) @@ -647,9 +734,9 @@ func TestWatch_UpdateFile_When_Skipped_But_Referenced(t *testing.T) { defer close(ch) tempDir := t.TempDir() - t.Cleanup(func() { os.RemoveAll(tempDir) }) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) - p := New(static.FileProvider{Directories: []string{tempDir}}) + p := New(static.FileProvider{Directories: []static.FileConfig{{Path: tempDir}}}) pool := safe.NewPool(context.Background()) defer func() { pool.Stop() @@ -687,7 +774,7 @@ func TestWatch_UpdateFile_When_Skipped_But_Referenced(t *testing.T) { func createAndStartFileProvider(t *testing.T, files ...string) (*Provider, chan dynamic.ConfigEvent) { tempDir := t.TempDir() - t.Cleanup(func() { os.RemoveAll(tempDir) }) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) for _, file := range files { if len(file) > 0 { @@ -712,7 +799,7 @@ func createDirectoryProvider(t *testing.T, files ...string) *Provider { for _, file := range files { tempDir := t.TempDir() dirs = append(dirs, tempDir) - t.Cleanup(func() { os.RemoveAll(tempDir) }) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) if len(file) > 0 { err := createTempFile(file, tempDir) @@ -720,7 +807,7 @@ func createDirectoryProvider(t *testing.T, files ...string) *Provider { } } - p := New(static.FileProvider{Directories: []string{strings.Join(dirs, string(os.PathListSeparator))}}) + p := New(static.FileProvider{Directories: []static.FileConfig{{Path: strings.Join(dirs, string(os.PathListSeparator))}}}) return p } diff --git a/config/dynamic/provider/git/git.go b/config/dynamic/provider/git/git.go index 6474d7d90..31724ec7a 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 @@ -188,7 +192,7 @@ func (p *Provider) initRepository(r *repository, ch chan dynamic.ConfigEvent, po } func (p *Provider) startFileProvider(dir string, ch chan dynamic.ConfigEvent, pool *safe.Pool) { - f := file.New(static.FileProvider{Directories: []string{dir}}) + f := file.New(static.FileProvider{Directories: []static.FileConfig{{Path: dir}}}) f.SkipPrefix = append(f.SkipPrefix, ".git") err := f.Start(ch, pool) if err != nil { @@ -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()) } } @@ -327,6 +329,11 @@ func wrapConfig(c *dynamic.Config, r *repository) { // to query git log takes too long Time: time.Now(), } + h, err := r.repo.Head() + if err == nil { + ref := h.Name() + info.Tags = append(info.Tags, ref.Short()) + } dynamic.Wrap(info, c) } diff --git a/config/dynamic/provider/git/git_test.go b/config/dynamic/provider/git/git_test.go index 5fba0a36a..8b83e698c 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{}{ @@ -54,6 +55,7 @@ func TestGit(t *testing.T) { require.Equal(t, "https://github.com/marle3003/mokapi-example.git?file=/"+name, c.Config.Info.Path()) require.Contains(t, gitFiles, name) require.NotNil(t, c.Config.Info.Checksum) + require.Equal(t, []string{"main"}, c.Config.Info.Tags) } } require.Equal(t, 4, count) @@ -225,8 +227,38 @@ Stop: require.Len(t, files, 1) } -// go-git requires git installed for file:// repositories -func testGitSimpleUrl(t *testing.T) { +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 + e := wait(ch, 3*time.Second) + initChecksum := e.Config.Info.Checksum + initTime := e.Config.Info.Time + + 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) + require.NotEqual(t, initChecksum, e.Config.Info.Checksum) + require.Greater(t, e.Config.Info.Time, initTime) +} + +func TestGitSimpleUrl(t *testing.T) { repo := newGitRepo(t, t.Name()) defer func() { err := os.RemoveAll(repo.dir) @@ -247,7 +279,7 @@ func testGitSimpleUrl(t *testing.T) { case <-time.After(1 * time.Second): t.Fatal("Timeout") case e := <-ch: - require.Equal(t, "foo.txt", filepath.Base(e.Config.Info.Url.String())) + require.Equal(t, "TestGitSimpleUrl?file=%2Ffoo.txt", filepath.Base(e.Config.Info.Url.String())) } } @@ -330,3 +362,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/config/static/static_config.go b/config/static/static_config.go index 38976a4ef..a29557435 100644 --- a/config/static/static_config.go +++ b/config/static/static_config.go @@ -10,6 +10,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" ) type Config struct { @@ -69,10 +70,17 @@ type Search struct { } type FileProvider struct { - Filenames []string `aliases:"filename" explode:"filename"` - Directories []string `aliases:"directory" explode:"directory"` - SkipPrefix []string `yaml:"skipPrefix" json:"skipPrefix" flag:"skip-prefix"` + Filenames []string `aliases:"filename" explode:"filename"` + Directories []FileConfig `aliases:"directory" explode:"directory"` + SkipPrefix []string `yaml:"skipPrefix" json:"skipPrefix" flag:"skip-prefix"` Include []string + Exclude []string +} + +type FileConfig struct { + Path string + Include []string + Exclude []string } type GitProvider struct { @@ -228,3 +236,51 @@ func (d *DataGen) OptionalPropertiesProbability() float64 { } return f } + +func (fc *FileConfig) UnmarshalYAML(value *yaml.Node) error { + if value.Kind == yaml.ScalarNode { + fc.Path = value.Value + return nil + } + type alias FileConfig + tmp := alias(*fc) + err := value.Decode(&tmp) + if err != nil { + return err + } + *fc = FileConfig(tmp) + return nil +} + +func (fc *FileConfig) UnmarshalJSON(b []byte) error { + switch b[0] { + case '"': + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + fc.Path = s + return nil + case '{': + dec := json.NewDecoder(bytes.NewReader(b)) + type alias FileConfig + tmp := alias(*fc) + err := dec.Decode(&tmp) + if err != nil { + return err + } + *fc = FileConfig(tmp) + return nil + default: + return fmt.Errorf("unexpected JSON type: %s", string(b)) + } +} + +func (fc *FileConfig) Set(v any) error { + switch x := v.(type) { + case string: + fc.Path = x + return nil + } + return fmt.Errorf("expected string, got %T", v) +} diff --git a/config/static/static_config_test.go b/config/static/static_config_test.go index a7cbf76e1..a1582cbb5 100644 --- a/config/static/static_config_test.go +++ b/config/static/static_config_test.go @@ -2,874 +2,128 @@ package static_test import ( "encoding/json" - "mokapi/config/decoders" - "mokapi/config/dynamic/provider/file/filetest" "mokapi/config/static" - "os" "testing" - "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) -func TestStaticConfig(t *testing.T) { +func TestYaml(t *testing.T) { testcases := []struct { name string - test func(t *testing.T) + data string + test func(t *testing.T, cfg *static.Config) }{ { - name: "assign with =", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--ConfigFile=foo`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, "foo", cfg.ConfigFile) - }, - }, - { - name: "assign without =", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--ConfigFile`, "foo") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, "foo", cfg.ConfigFile) - }, - }, - { - name: "--help", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--help`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, true, cfg.Help) - }, - }, - { - name: "-h", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `-h`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, true, cfg.Help) - }, - }, - { - name: "--version", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--version`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, true, cfg.Version) - }, - }, - { - name: "-v", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `-v`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, true, cfg.Version) - }, - }, - { - name: "json", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--providers.file={"filename":"foo.yaml","directory":"foo", "skipPrefix":["_"]}`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, []string{"foo.yaml"}, cfg.Providers.File.Filenames) - require.Equal(t, []string{"foo"}, cfg.Providers.File.Directories) - require.Equal(t, []string{"_"}, cfg.Providers.File.SkipPrefix) - }, - }, - { - name: "shorthand object", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--providers.file`, "filename=foo.yaml") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, []string{"foo.yaml"}, cfg.Providers.File.Filenames) - }, - }, - { - name: "args", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--providers.git.repositories[0].url=https://github.com/PATH-TO/REPOSITORY?ref=branch-name") - os.Args = append(os.Args, "--providers.git.repositories[0].pullInterval=5m") - os.Args = append(os.Args, "--providers.git.repositories[1].pullInterval=5h") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, "https://github.com/PATH-TO/REPOSITORY?ref=branch-name", cfg.Providers.Git.Repositories[0].Url) - require.Equal(t, "5m", cfg.Providers.Git.Repositories[0].PullInterval) - require.Equal(t, "", cfg.Providers.Git.Repositories[1].Url) - require.Equal(t, "5h", cfg.Providers.Git.Repositories[1].PullInterval) - }, - }, - { - name: "shorthand array", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--providers.git.repositories`, "url=foo,pullInterval=5m url=bar,pullInterval=5h") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Len(t, cfg.Providers.Git.Repositories, 2) - require.Equal(t, "foo", cfg.Providers.Git.Repositories[0].Url) - require.Equal(t, "5m", cfg.Providers.Git.Repositories[0].PullInterval) - require.Equal(t, "bar", cfg.Providers.Git.Repositories[1].Url) - require.Equal(t, "5h", cfg.Providers.Git.Repositories[1].PullInterval) - }, - }, - { - name: "explode with json", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, `--providers.git.repository={"url":"https://github.com/PATH-TO/REPOSITORY?ref=branch-name","pullInterval":"5m"}`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - require.Equal(t, "https://github.com/PATH-TO/REPOSITORY?ref=branch-name", cfg.Providers.Git.Repositories[0].Url) - require.Equal(t, "5m", cfg.Providers.Git.Repositories[0].PullInterval) - }, - }, - { - name: "env var", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_Providers_GIT_Repositories[0]_Url", "https://github.com/PATH-TO/REPOSITORY") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_Providers_GIT_Repositories[0]_Url") - err = os.Setenv("MOKAPI_Providers_GIT_Repositories[0]_PullInterval", "3m") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_Providers_GIT_Repositories[0]_PullInterval") - - cfg := static.Config{} - err = decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, "https://github.com/PATH-TO/REPOSITORY", cfg.Providers.Git.Repositories[0].Url) - require.Equal(t, "3m", cfg.Providers.Git.Repositories[0].PullInterval) - }, - }, - { - name: "api port as env var", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_API_PORT", "1234") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_API_PORT") - - cfg := static.Config{} - err = decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, 1234, cfg.Api.Port) - }, - }, - { - name: "file provider include", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.file.include", `mokapi/**/*.json mokapi/**/*.yaml "foo bar/**/*.yaml"`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"mokapi/**/*.json", "mokapi/**/*.yaml", "foo bar/**/*.yaml"}, cfg.Providers.File.Include) - }, - }, - { - name: "file provider include with space", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.file.include", `"C:\Documents and Settings\" C:\Work"`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"C:\\Documents and Settings\\", "C:\\Work"}, cfg.Providers.File.Include) - }, - }, - { - name: "file provider include twice", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.file.include", "foo", "--Providers.file.include", "bar") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo", "bar"}, cfg.Providers.File.Include) - }, - }, - { - name: "file provider include overwrite", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.file.include", "foo", "--Providers.file.include[0]", "bar") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"bar"}, cfg.Providers.File.Include) - }, - }, - { - name: "git provider set url", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.git.Url", `foo`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo"}, cfg.Providers.Git.Urls) - }, - }, - { - name: "git provider set urls", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.git.Urls", `foo`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo"}, cfg.Providers.Git.Urls) - }, - }, - { - name: "http provider set url", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.Http.Url", `foo`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo"}, cfg.Providers.Http.Urls) - }, - }, - { - name: "http provider set urls using explode", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.Http.Url", `foo`) - os.Args = append(os.Args, "--Providers.Http.Url", `bar`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo", "bar"}, cfg.Providers.Http.Urls) - }, - }, - { - name: "http provider set urls", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.Http.Urls", `foo bar`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo", "bar"}, cfg.Providers.Http.Urls) - }, - }, - { - name: "http provider", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--Providers.Http", `urls=foo bar,pollInterval=5s,pollTimeout=30s,proxy=bar,tlsSkipVerify=true`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo", "bar"}, cfg.Providers.Http.Urls) - require.Equal(t, "5s", cfg.Providers.Http.PollInterval) - require.Equal(t, "30s", cfg.Providers.Http.PollTimeout) - require.Equal(t, true, cfg.Providers.Http.TlsSkipVerify) - require.Equal(t, "bar", cfg.Providers.Http.Proxy) - }, - }, - { - name: "npm provider global folders", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--providers-npm-global-folders", `/etc/foo`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, "/etc/foo", cfg.Providers.Npm.GlobalFolders[0]) - }, - }, - { - name: "config", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--config", `{"openapi": "3.0"}`) - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Len(t, cfg.Configs, 1) - require.Equal(t, "{\"openapi\": \"3.0\"}", cfg.Configs[0]) - }, - }, - { - name: "config file://", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, "--config", `file://C:/temp/patch.yaml`) - - cfg := static.Config{} - err := decoders.Load( - []decoders.ConfigDecoder{ - decoders.NewFlagDecoderWithReader(&filetest.MockFS{ - Entries: []*filetest.Entry{ - { - Name: "/temp/patch.yaml", - Data: []byte("{\"openapi\": \"3.0\"}"), - }, - }, - WorkingDir: "", - })}, &cfg) - require.NoError(t, err) - - require.Len(t, cfg.Configs, 1) - require.Equal(t, "{\"openapi\": \"3.0\"}", cfg.Configs[0]) - }, - }, - { - name: "configfile json", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--configfile", "foo.json") - - read := func(path string) ([]byte, error) { - return []byte(`{"configs": [ { "openapi": "3.0", "info": { "name": "foo" } } ]}`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Len(t, cfg.Configs, 1) - - actual := map[string]interface{}{} - err = json.Unmarshal([]byte(cfg.Configs[0]), &actual) - require.NoError(t, err) - expected := map[string]interface{}{ - "openapi": "3.0", - "info": map[string]interface{}{ - "name": "foo", - }, - } - require.Equal(t, expected, actual) - }, - }, - { - name: "configfile yaml", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--configfile", "foo.yaml") - - read := func(path string) ([]byte, error) { - return []byte(` -configs: - - openapi: "3.0" - info: - name: foo -`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - actual := map[string]interface{}{} - err = json.Unmarshal([]byte(cfg.Configs[0]), &actual) - require.NoError(t, err) - expected := map[string]interface{}{ - "openapi": "3.0", - "info": map[string]interface{}{ - "name": "foo", - }, - } - - require.Len(t, cfg.Configs, 1) - require.Equal(t, expected, actual) - }, - }, - { - name: "config-file", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--config-file", "foo.json") - - read := func(path string) ([]byte, error) { - return []byte(`{"log": { "level": "error" } }`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Equal(t, "error", cfg.Log.Level) - }, - }, - { - name: "cli-input", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--cli-input", "foo.json") - - read := func(path string) ([]byte, error) { - return []byte(`{"log": { "level": "error" } }`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Equal(t, "error", cfg.Log.Level) - }, - }, - { - name: "cli-input file provider directories", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--cli-input", "foo.yaml") - - read := func(path string) ([]byte, error) { - return []byte(` + name: "directories as string array", + data: ` providers: file: - directory: foo -`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo"}, cfg.Providers.File.Directories) + directories: + - ./dir +`, + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{{Path: "./dir"}}, cfg.Providers.File.Directories) }, }, { - name: "cli-input file provider directories", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--cli-input", "foo.yaml") - - read := func(path string) ([]byte, error) { - return []byte(` + name: "directories as file config", + data: ` providers: file: - directories: ["/foo", "/bar"] -`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"/foo", "/bar"}, cfg.Providers.File.Directories) - }, - }, - { - name: "cli-input file provider directory", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--cli-input", "foo.json") - - read := func(path string) ([]byte, error) { - return []byte(`{"providers":{"file":{"directory":"foo"}}}`), nil - } - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read), decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - - require.Equal(t, []string{"foo"}, cfg.Providers.File.Directories) - }, - }, - { - name: "positional parameter file", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "foo.json") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.NoError(t, err) - - require.Equal(t, []string{"foo.json"}, cfg.Args) - require.Equal(t, "foo.json", cfg.Providers.File.Filenames[0]) - }, - }, - { - name: "positional parameter http", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "http://foo.io/foo.json") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.NoError(t, err) - - require.Equal(t, "http://foo.io/foo.json", cfg.Providers.Http.Urls[0]) - }, - }, - { - name: "positional parameter https", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "https://foo.io/foo.json") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.NoError(t, err) - - require.Equal(t, "https://foo.io/foo.json", cfg.Providers.Http.Urls[0]) - }, - }, - { - name: "positional parameter git with https", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "git+https://foo.io/foo.json") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.NoError(t, err) - - require.Equal(t, "https://foo.io/foo.json", cfg.Providers.Git.Urls[0]) - }, - }, - { - name: "positional parameter npm", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "npm://bar/foo.txt?scope=@foo") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.NoError(t, err) - - require.Equal(t, "npm://bar/foo.txt?scope=@foo", cfg.Providers.Npm.Packages[0].Name) - }, - }, - { - name: "positional parameter not supported", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "foo://bar") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.EqualError(t, err, "positional argument is not supported: foo://bar") - }, - }, - { - name: "positional parameter Windows path", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "C:\\bar") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, "C:\\bar", cfg.Providers.File.Filenames[0]) - }, - }, - { - name: "not defined data-gen optional properties", - test: func(t *testing.T) { - hook := test.NewGlobal() - - os.Args = append(os.Args, "mokapi.exe") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, 0.85, cfg.DataGen.OptionalPropertiesProbability()) - require.Len(t, hook.Entries, 0) - }, - }, - { - name: "data-gen optional properties", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--data-gen-optionalProperties", "often") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, 0.85, cfg.DataGen.OptionalPropertiesProbability()) - }, - }, - { - name: "data-gen optional properties always", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--data-gen-optionalProperties", "always") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, 1.0, cfg.DataGen.OptionalPropertiesProbability()) - }, - }, - { - name: "data-gen optional properties sometimes", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--data-gen-optionalProperties", "sometimes") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, 0.5, cfg.DataGen.OptionalPropertiesProbability()) - }, - }, - { - name: "data-gen optional properties 0.3", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe", "--data-gen-optionalProperties", "0.3") - - cfg := static.Config{} - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFlagDecoder()}, &cfg) - require.NoError(t, err) - err = cfg.Parse() - require.Equal(t, 0.3, cfg.DataGen.OptionalPropertiesProbability()) + directories: + - path: ./dir +`, + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{{Path: "./dir"}}, cfg.Providers.File.Directories) }, }, { - name: "data-gen env var", - test: func(t *testing.T) { - os.Args = append(os.Args, "mokapi.exe") - err := os.Setenv("MOKAPI_DATA_GEN_OPTIONALPROPERTIES", "sometimes") - require.NoError(t, err) - defer os.Unsetenv("MOKAPI_DATA_GEN_OPTIONALPROPERTIES") - - cfg := static.Config{} - err = decoders.Load([]decoders.ConfigDecoder{&decoders.FlagDecoder{}}, &cfg) - require.NoError(t, err) - - require.Equal(t, 0.5, cfg.DataGen.OptionalPropertiesProbability()) + name: "directories as mixed", + data: ` +providers: + file: + directories: + - path: ./dir + include: ['*.js'] + - ./foo +`, + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{ + {Path: "./dir", Include: []string{"*.js"}}, + {Path: "./foo"}, + }, cfg.Providers.File.Directories) }, }, } for _, tc := range testcases { - tc := tc t.Run(tc.name, func(t *testing.T) { - os.Args = nil - tc.test(t) + var cfg *static.Config + err := yaml.Unmarshal([]byte(tc.data), &cfg) + require.NoError(t, err) + tc.test(t, cfg) }) } } -func TestFileProvider(t *testing.T) { +func TestJson(t *testing.T) { testcases := []struct { name string - args []string - test func(t *testing.T, cfg *static.Config, err error) + data string + test func(t *testing.T, cfg *static.Config) }{ { - name: "skipPrefix single element appends to default value", - args: []string{"--providers-file-skip-prefix", "foo"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Len(t, cfg.Providers.File.SkipPrefix, 2) - require.Contains(t, cfg.Providers.File.SkipPrefix, "foo") - require.Contains(t, cfg.Providers.File.SkipPrefix, "_") - }, - }, - { - name: "skipPrefix list replace default value", - args: []string{"--providers-file-skip-prefix", "foo", "bar"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Len(t, cfg.Providers.File.SkipPrefix, 2) - require.Contains(t, cfg.Providers.File.SkipPrefix, "foo") - require.Contains(t, cfg.Providers.File.SkipPrefix, "bar") - }, - }, - { - name: "feature foo", - args: []string{"--feature", "foo"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Len(t, cfg.Features, 1) - require.Equal(t, "foo", cfg.Features[0]) - }, - }, - { - name: "event store size", - args: []string{"--event-store-default", "size=200"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(200), cfg.Event.Store["default"].Size) - }, - }, - { - name: "event store size", - args: []string{"--event-store-default-size", "200"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(200), cfg.Event.Store["default"].Size) - }, - }, - { - name: "default event store size", - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(100), cfg.Event.Store["default"].Size) - }, - }, - { - name: "event store for foo", - args: []string{"--event-store-foo-size", "250"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(250), cfg.Event.Store["foo"].Size) - }, - }, - { - name: "event store name with spaces", - args: []string{"--event-store", `Swagger PetStore API={"size": 250}`}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(250), cfg.Event.Store["Swagger PetStore API"].Size) - }, - }, - { - name: "event store name with spaces followed by positional parameter: error", - args: []string{"--event-store", `Swagger PetStore API={"size": 250}`, "smtp.yaml"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.EqualError(t, err, "configuration error 'event-store' value '[Swagger PetStore API={\"size\": 250} smtp.yaml]': expected key to set map value") - }, - }, - { - name: "event store name with spaces and positional parameter", - args: []string{"--event-store", `Swagger PetStore API={"size": 250}`, "--", "smtp.yaml"}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(250), cfg.Event.Store["Swagger PetStore API"].Size) + name: "directories as string array", + data: `{ +"providers": { + "file": { + "directories": ["./dir"] + } +}} +`, + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{{Path: "./dir"}}, cfg.Providers.File.Directories) }, }, { - name: "positional parameter followed event store name with spaces and ", - args: []string{"smtp.yaml", "--event-store", `Swagger PetStore API={"size": 250}`}, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Equal(t, int64(250), cfg.Event.Store["Swagger PetStore API"].Size) - }, - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - os.Args = nil - os.Args = append(os.Args, "mokapi.exe") - os.Args = append(os.Args, tc.args...) - - cfg := static.NewConfig() - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewDefaultFileDecoder(), decoders.NewFlagDecoder()}, cfg) - - tc.test(t, cfg, err) - }) - } -} - -func TestFileProvider_File(t *testing.T) { - testcases := []struct { - name string - content string - test func(t *testing.T, cfg *static.Config, err error) - }{ - { - name: "empty file", - content: "", - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) + name: "directories as file config", + data: `{ +"providers": { + "file": { + "directories": [{"path":"./dir"}] + } +}} +`, + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{{Path: "./dir"}}, cfg.Providers.File.Directories) }, }, { - name: "git repo with GitHub Auth", - content: ` -providers: - git: - repositories: - - auth: - github: - appId: 1234 + name: "directories as mixed", + data: `{ +"providers": { + "file": { + "directories": [{"path":"./dir","include":["*.js"]}, "./foo"] + } +}} `, - test: func(t *testing.T, cfg *static.Config, err error) { - require.NoError(t, err) - require.Len(t, cfg.Providers.Git.Repositories, 1) - require.NotNil(t, cfg.Providers.Git.Repositories[0].Auth.GitHub) - require.Equal(t, int64(1234), cfg.Providers.Git.Repositories[0].Auth.GitHub.AppId) + test: func(t *testing.T, cfg *static.Config) { + require.Equal(t, []static.FileConfig{ + {Path: "./dir", Include: []string{"*.js"}}, + {Path: "./foo"}, + }, cfg.Providers.File.Directories) }, }, } for _, tc := range testcases { - tc := tc t.Run(tc.name, func(t *testing.T) { - cfg := static.NewConfig() - - read := func(path string) ([]byte, error) { - return []byte(tc.content), nil - } - - err := decoders.Load([]decoders.ConfigDecoder{decoders.NewFileDecoder(read)}, cfg) - - tc.test(t, cfg, err) + var cfg *static.Config + err := json.Unmarshal([]byte(tc.data), &cfg) + require.NoError(t, err) + tc.test(t, cfg) }) } } diff --git a/docs/config.json b/docs/config.json index 9f8589d3a..92ec25bc3 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1,223 +1,660 @@ { - "Guides": { - "items": { - "Welcome": "guides/get-started/welcome.md", - "Get Started": { - "items": { - "Installation": "guides/get-started/installation.md", - "Running": "guides/get-started/running.md", - "Test Data": "guides/get-started/test-data.md", - "Dashboard": "guides/get-started/dashboard.md" - } + "Docs": { + "type": "root", + "items": [ + { + "label": "Welcome", + "source": "get-started/welcome.md", + "path": "/docs/welcome" }, - "HTTP": { - "index": "guides/http/overview.md", - "items": { - "Quick Start": "guides/http/quick-start.md", - "HTTPS & TLS": "guides/http/tls.md", - "Dashboard": "guides/http/dashboard.md" - } - }, - "Kafka": { - "index": "guides/kafka/overview.md", - "items": { - "Quick Start": "guides/kafka/quick-start.md", - "Config": "guides/kafka/config.md" - } - }, - "LDAP": { - "index": "guides/ldap/intro.md", - "items": { - "Quick Start": "guides/ldap/quick-start.md" - } - }, - "Mail": { - "index": "guides/mail/overview.md", - "items": { - "Quick Start": "guides/mail/quick-start.md", - "Client": "guides/mail/client.md", - "Rules": "guides/mail/rules.md" - } - } - } - }, - "Configuration": { - "items": { - "Introduction": "configuration/introduction.md", - "Patching": "configuration/patching.md", - "Static": { - "expanded": true, - "items": { - "CLI Usage": "configuration/static/cli.md" - } - }, - "Dynamic": { - "expanded": true, - "items": { - "Overview": "configuration/dynamic/overview.md", - "File": "configuration/dynamic/file.md", - "HTTP": "configuration/dynamic/http.md", - "GIT": "configuration/dynamic/git.md", - "NPM": "configuration/dynamic/npm.md" - } - }, - "Reference": "configuration/reference.md" - } - }, - "JavaScript API": { - "items": { - "Overview": "javascript-api/overview.md", - "Modules": "javascript-api/modules.md", - "globals": { - "items": { - "open": "javascript-api/open.md" - } - }, - "mokapi": { - "items": { - "cron": "javascript-api/mokapi/cron.md", - "date": "javascript-api/mokapi/date.md", - "env": "javascript-api/mokapi/env.md", - "every": "javascript-api/mokapi/every.md", - "on": "javascript-api/mokapi/on.md", - "sleep": "javascript-api/mokapi/sleep.md", - "marshal": "javascript-api/mokapi/marshal.md", - "EventHandler": { - "index": "javascript-api/mokapi/eventhandler/eventhandler.md", - "items": { - "EventArgs": "javascript-api/mokapi/eventhandler/eventargs.md", - "HttpEventHandler": "javascript-api/mokapi/eventhandler/httpeventhandler.md", - "HttpRequest": "javascript-api/mokapi/eventhandler/httprequest.md", - "HttpResponse": "javascript-api/mokapi/eventhandler/httpresponse.md", - "KafkaEventHandler": "javascript-api/mokapi/eventhandler/kafkaeventhandler.md", - "KafkaEventMessage": "javascript-api/mokapi/eventhandler/kafkaeventmessage.md", - "ScheduledEventArgs": "javascript-api/mokapi/eventhandler/scheduledeventargs.md" - } + { + "label": "Get Started", + "type": "headline", + "items": [ + { + "label": "Installation", + "source": "get-started/installation.md", + "path": "/docs/get-started/installation" + }, + { + "label": "Running", + "source": "get-started/running.md", + "path": "/docs/get-started/running" + }, + { + "label": "Test Data", + "source": "get-started/test-data.md", + "path": "/docs/get-started/test-data" + }, + { + "label": "Dashboard", + "source": "get-started/dashboard.md", + "path": "/docs/get-started/dashboard" } - } - }, - "mokapi/faker": { - "items": { - "fake": "javascript-api/mokapi-faker/fake.md" - } + ] }, - "mokapi/http": { - "items": { - "get": "javascript-api/mokapi-http/get.md", - "post": "javascript-api/mokapi-http/post.md", - "put": "javascript-api/mokapi-http/put.md", - "head": "javascript-api/mokapi-http/head.md", - "delete": "javascript-api/mokapi-http/delete.md", - "options": "javascript-api/mokapi-http/options.md", - "patch": "javascript-api/mokapi-http/patch.md", - "fetch": "javascript-api/mokapi-http/fetch.md", - "HttpResponse": "javascript-api/mokapi-http/httpresponse.md", - "FetchOptions": "javascript-api/mokapi-http/fetchoptions.md", - "Args": "javascript-api/mokapi-http/args.md" - } - }, - "mokapi/kafka": { - "items": { - "produce": "javascript-api/mokapi-kafka/produce.md", - "ProduceArgs": "javascript-api/mokapi-kafka/produceargs.md", - "ProduceResult": "javascript-api/mokapi-kafka/produceresult.md", - "Message": "javascript-api/mokapi-kafka/message.md", - "MessageResult": "javascript-api/mokapi-kafka/messageresult.md", - "ProduceRetry": "javascript-api/mokapi-kafka/produceretry.md" - } - }, - "mokapi/mustache": { - "items": { - "render": "javascript-api/mokapi-mustache/render.md" - } + { + "label": "Protocol", + "type": "headline", + "items": [ + { + "label": "HTTP", + "items": [ + { + "label": "Overview", + "source": "http/overview.md", + "path": "/docs/http/overview" + }, + { + "label": "Quick Start", + "source": "http/quick-start.md", + "path": "/docs/http/quick-start" + }, + { + "label": "HTTPS & TLS", + "source": "http/tls.md", + "path": "/docs/http/tls" + }, + { + "label": "Dashboard", + "source": "http/dashboard.md", + "path": "/docs/http/dashboard" + } + ] + }, + { + "label": "Kafka", + "items": [ + { + "label": "Overview", + "source": "kafka/overview.md", + "path": "/docs/kafka/overview" + }, + { + "label": "Quick Start", + "source": "kafka/quick-start.md", + "path": "/docs/kafka/quick-start" + }, + { + "label": "Config", + "source": "kafka/config.md", + "path": "/docs/kafka/config" + } + ] + }, + { + "label": "LDAP", + "items": [ + { + "label": "Overview", + "source": "ldap/intro.md", + "path": "/docs/ldap/overview" + }, + { + "label": "Quick Start", + "source": "ldap/quick-start.md", + "path": "/docs/ldap/quick-start" + } + ] + }, + { + "label": "Mail", + "items": [ + { + "label": "Overview", + "source": "mail/overview.md", + "path": "/docs/mail/overview" + }, + { + "label": "Clients", + "source": "mail/client.md", + "path": "/docs/mail/client" + }, + { + "label": "Rules", + "source": "mail/rules.md", + "path": "/docs/mail/rules" + } + ] + } + ] }, - "mokapi/yaml": { - "items": { - "parse": "javascript-api/mokapi-yaml/parse.md", - "stringify": "javascript-api/mokapi-yaml/stringify.md" - } + { + "label": "Configuration", + "type": "headline", + "items": [ + { + "label": "Overview", + "source": "configuration/introduction.md", + "path": "/docs/configuration/overview" + }, + { + "label": "Patching", + "source": "configuration/patching.md", + "path": "/docs/configuration/patching" + }, + { + "label": "Static", + "items": [ + { + "label": "CLI Usage", + "source": "configuration/static/cli.md", + "path": "/docs/configuration/static/cli-usage" + }, + { + "label": "CLI Flags", + "source": "configuration/static/mokapi.md", + "path": "/docs/configuration/static/cli-flags" + } + ] + }, + { + "label": "Dynamic", + "items": [ + { + "label": "Overview", + "source": "configuration/dynamic/overview.md", + "path": "/docs/configuration/dynamic/overview" + }, + { + "label": "File", + "source": "configuration/dynamic/file.md", + "path": "/docs/configuration/dynamic/file" + }, + { + "label": "HTTP", + "source": "configuration/dynamic/http.md", + "path": "/docs/configuration/dynamic/http" + }, + { + "label": "GIT", + "source": "configuration/dynamic/git.md", + "path": "/docs/configuration/dynamic/git" + }, + { + "label": "NPM", + "source": "configuration/dynamic/npm.md", + "path": "/docs/configuration/dynamic/npm" + }, + { + "label": "Reference", + "source": "configuration/reference.md", + "path": "/docs/configuration/reference" + } + ] + } + ] }, - "mokapi/encoding": { - "items": { - "base64 encode": "javascript-api/mokapi-encoding/base64-encode.md", - "base64 decode": "javascript-api/mokapi-encoding/base64-decode.md" - } + { + "label": "JavaScript API", + "type": "headline", + "items": [ + { + "label": "Overview", + "source": "javascript-api/overview.md", + "path": "/docs/javascript-api/overview" + }, + { + "label": "Modules", + "source": "javascript-api/modules.md", + "path": "/docs/javascript-api/modules" + }, + { + "label": "globals", + "items": [ + { + "label": "open", + "source": "javascript-api/open.md", + "path": "/docs/javascript-api/open" + } + ] + }, + { + "label": "mokapi", + "items": [ + { + "label": "cron", + "source": "javascript-api/mokapi/cron.md", + "path": "/docs/javascript-api/mokapi/cron" + }, + { + "label": "date", + "source": "javascript-api/mokapi/date.md", + "path": "/docs/javascript-api/mokapi/date" + }, + { + "label": "env", + "source": "javascript-api/mokapi/env.md", + "path": "/docs/javascript-api/mokapi/env" + }, + { + "label": "every", + "source": "javascript-api/mokapi/every.md", + "path": "/docs/javascript-api/mokapi/every" + }, + { + "label": "on", + "source": "javascript-api/mokapi/on.md", + "path": "/docs/javascript-api/mokapi/on" + }, + { + "label": "sleep", + "source": "javascript-api/mokapi/sleep.md", + "path": "/docs/javascript-api/mokapi/sleep" + }, + { + "label": "marshal", + "source": "javascript-api/mokapi/marshal.md", + "path": "/docs/javascript-api/mokapi/marshal" + }, + { + "label": "EventHandler", + "items": [ + { + "label": "Overview", + "source": "javascript-api/mokapi/eventhandler/eventhandler.md", + "path": "/docs/javascript-api/mokapi/eventhandler/eventhandler" + }, + { + "label": "EventArgs", + "source": "javascript-api/mokapi/eventhandler/eventargs.md", + "path": "/docs/javascript-api/mokapi/eventhandler/eventargs" + }, + { + "label": "HttpEventHandler", + "source": "javascript-api/mokapi/eventhandler/httpeventhandler.md", + "path": "/docs/javascript-api/mokapi/eventhandler/httpeventhandler" + }, + { + "label": "HttpRequest", + "source": "javascript-api/mokapi/eventhandler/httprequest.md", + "path": "/docs/javascript-api/mokapi/eventhandler/httprequest" + }, + { + "label": "HttpResponse", + "source": "javascript-api/mokapi/eventhandler/httpresponse.md", + "path": "/docs/javascript-api/mokapi/eventhandler/httpresponse" + }, + { + "label": "KafkaEventHandler", + "source": "javascript-api/mokapi/eventhandler/kafkaeventhandler.md", + "path": "/docs/javascript-api/mokapi/eventhandler/kafkaeventhandler" + }, + { + "label": "KafkaEventMessage", + "source": "javascript-api/mokapi/eventhandler/kafkaeventmessage.md", + "path": "/docs/javascript-api/mokapi/eventhandler/kafkaeventmessage" + }, + { + "label": "KafkaEventMessage", + "source": "javascript-api/mokapi/eventhandler/scheduledeventargs.md", + "path": "/docs/javascript-api/mokapi/eventhandler/scheduledeventargs" + } + ] + } + ] + }, + { + "label": "mokapi/faker", + "items": [ + { + "label": "fake", + "source": "javascript-api/mokapi-faker/fake.md", + "path": "/docs/javascript-api/mokapi-faker/fake" + } + ] + }, + { + "label": "mokapi/http", + "items": [ + { + "label": "get", + "source": "javascript-api/mokapi-http/get.md", + "path": "/docs/javascript-api/mokapi-http/get" + }, + { + "label": "post", + "source": "javascript-api/mokapi-http/post.md", + "path": "/docs/javascript-api/mokapi-http/post" + }, + { + "label": "put", + "source": "javascript-api/mokapi-http/put.md", + "path": "/docs/javascript-api/mokapi-http/put" + }, + { + "label": "head", + "source": "javascript-api/mokapi-http/head.md", + "path": "/docs/javascript-api/mokapi-http/head" + }, + { + "label": "delete", + "source": "javascript-api/mokapi-http/delete.md", + "path": "/docs/javascript-api/mokapi-http/delete" + }, + { + "label": "options", + "source": "javascript-api/mokapi-http/options.md", + "path": "/docs/javascript-api/mokapi-http/options" + }, + { + "label": "patch", + "source": "javascript-api/mokapi-http/patch.md", + "path": "/docs/javascript-api/mokapi-http/patch" + }, + { + "label": "fetch", + "source": "javascript-api/mokapi-http/fetch.md", + "path": "/docs/javascript-api/mokapi-http/fetch" + }, + { + "label": "HttpResponse", + "source": "javascript-api/mokapi-http/httpresponse.md", + "path": "/docs/javascript-api/mokapi-http/httpresponse" + }, + { + "label": "FetchOptions", + "source": "javascript-api/mokapi-http/fetchoptions.md", + "path": "/docs/javascript-api/mokapi-http/fetchoptions" + }, + { + "label": "Args", + "source": "javascript-api/mokapi-http/args.md", + "path": "/docs/javascript-api/mokapi-http/args" + } + ] + }, + { + "label": "mokapi/kafka", + "items": [ + { + "label": "produce", + "source": "javascript-api/mokapi-kafka/produce.md", + "path": "/docs/javascript-api/mokapi-kafka/produce" + }, + { + "label": "ProduceArgs", + "source": "javascript-api/mokapi-kafka/produceargs.md", + "path": "/docs/javascript-api/mokapi-kafka/produceargs" + }, + { + "label": "ProduceResult", + "source": "javascript-api/mokapi-kafka/produceresult.md", + "path": "/docs/javascript-api/mokapi-kafka/produceresult" + }, + { + "label": "Message", + "source": "javascript-api/mokapi-kafka/message.md", + "path": "/docs/javascript-api/mokapi-kafka/message" + }, + { + "label": "MessageResult", + "source": "javascript-api/mokapi-kafka/messageresult.md", + "path": "/docs/javascript-api/mokapi-kafka/messageresult" + }, + { + "label": "ProduceRetry", + "source": "javascript-api/mokapi-kafka/produceretry.md", + "path": "/docs/javascript-api/mokapi-kafka/produceretry" + } + ] + }, + { + "label": "mokapi/mustache", + "items": [ + { + "label": "render", + "source": "javascript-api/mokapi-mustache/render.md", + "path": "/docs/javascript-api/mokapi-mustache/render" + } + ] + }, + { + "label": "mokapi/yaml", + "items": [ + { + "label": "parse", + "source": "javascript-api/mokapi-yaml/parse.md", + "path": "/docs/javascript-api/mokapi-yaml/parse" + }, + { + "label": "stringify", + "source": "javascript-api/mokapi-yaml/stringify.md", + "path": "/docs/javascript-api/mokapi-yaml/stringify" + } + ] + }, + { + "label": "mokapi/encoding", + "items": [ + { + "label": "base64 encode", + "source": "javascript-api/mokapi-encoding/base64-encode.md", + "path": "/docs/javascript-api/mokapi-encoding/base64-encode" + }, + { + "label": "base64 decode", + "source": "javascript-api/mokapi-encoding/base64-decode.md", + "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" + } + ] + } + ] } - } + ] }, "Resources": { + "path": "/resources", + "type": "root", "index": { "component": "examples", "hideNavigation": true, "hideInNavigation": true, - "canonical": "https://mokapi.io/docs/resources", + "canonical": "https://mokapi.io/resources", "title": "Explore Mokapi Resources: Tutorials, Examples, and Blog Articles", "description": "Explore Mokapi's resources including tutorials, examples, and blog articles. Learn to mock APIs, validate schemas, and streamline your development." }, - "items": { - "Tutorials": { + "items": [ + { + "label": "Tutorials", + "path": "/resources/tutorials", "index": { "component": "examples", "hideNavigation": true, "hideInNavigation": true, - "canonical": "https://mokapi.io/docs/resources/tutorials", + "canonical": "https://mokapi.io/resources/tutorials", "title": "Explore Mokapi Resources: Tutorials, Examples, and Blog Articles", "description": "Explore Mokapi's resources including tutorials, examples, and blog articles. Learn to mock APIs, validate schemas, and streamline your development." }, - "items": { - "Get started with REST API": "resources/tutorials/simple-http-api.md", - "Mock OpenAPI Authentication API Key & Bearer Token": "resources/tutorials/mock-openapi-auth-apikey-bearer.md", - "Mock OpenAPI Authentication OAuth2 and JWT": "resources/tutorials/mock-oauth2-jwt-api.md", - "Get started with Kafka": "resources/tutorials/simple-kafka-api.md", - "Running Mokapi in a CI/CD Pipeline": "resources/tutorials/running-mokapi-github-action.md", - "Mock LDAP Authentication in Node": "resources/tutorials/mock-ldap-authentication.md", - "Mock LDAP Group Permission in Node": "resources/tutorials/mock-ldap-group-permission.md", - "Mock SMTP Server send Mail using Node": "resources/tutorials/mock-smtp-server-send-mail-using-node.md", - "Using Mokapi in Local Tests": "resources/tutorials/mokapi-in-locale-tests.md", - "Fullstack Dev Made Easier": "resources/tutorials/fullstack-dev-made-easier.md" - } + "items": [ + { + "label": "Get started with REST API", + "source": "resources/tutorials/simple-http-api.md", + "path": "/resources/tutorials/get-started-with-rest-api", + "hideNavigation": true + }, + { + "label": "Mock OpenAPI Authentication API Key & Bearer Token", + "source": "resources/tutorials/mock-openapi-auth-apikey-bearer.md", + "path": "/resources/tutorials/mock-openapi-auth-apikey-bearer", + "hideNavigation": true + }, + { + "label": "Mock OpenAPI Authentication OAuth2 and JWT", + "source": "resources/tutorials/mock-oauth2-jwt-api.md", + "path": "/resources/tutorials/mock-oauth2-jwt-api", + "hideNavigation": true + }, + { + "label": "Get started with Kafka", + "source": "resources/tutorials/simple-kafka-api.md", + "path": "/resources/tutorials/get-started-with-kafka", + "hideNavigation": true + }, + { + "label": "Running Mokapi in a CI/CD Pipeline", + "source": "resources/tutorials/running-mokapi-github-action.md", + "path": "/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline", + "hideNavigation": true + }, + { + "label": "Mock LDAP Authentication in Node", + "source": "resources/tutorials/mock-ldap-authentication.md", + "path": "/resources/tutorials/mock-ldap-authentication-in-node", + "hideNavigation": true + }, + { + "label": "Mock LDAP Group Permission in Node", + "source": "resources/tutorials/mock-ldap-group-permission.md", + "path": "/resources/tutorials/mock-ldap-group-permission-in-node", + "hideNavigation": true + }, + { + "label": "Mock SMTP Server send Mail using Node", + "source": "resources/tutorials/mock-smtp-server-send-mail-using-node.md", + "path": "/resources/tutorials/mock-smtp-server-send-mail-using-node", + "hideNavigation": true + }, + { + "label": "Using Mokapi in Local Tests", + "source": "resources/tutorials/mokapi-in-locale-tests.md", + "path": "/resources/tutorials/mokapi-in-locale-tests", + "hideNavigation": true + }, + { + "label": "Fullstack Dev Made Easier", + "source": "resources/tutorials/fullstack-dev-made-easier.md", + "path": "/resources/tutorials/fullstack-dev-made-easier", + "hideNavigation": true + } + ] }, - "Examples": { + { + "label": "Examples", + "path": "/resources/examples", "index": { "component": "examples", "hideNavigation": true, "hideInNavigation": true, - "canonical": "https://mokapi.io/docs/resources/examples", + "canonical": "https://mokapi.io/resources/examples", "title": "Explore Mokapi Resources: Tutorials, Examples, and Blog Articles", "description": "Explore Mokapi's resources including tutorials, examples, and blog articles. Learn to mock APIs, validate schemas, and streamline your development." }, - "items": { - "Mokapi behind Reverse Proxy": "resources/examples/mokapi-behind-proxy.md", - "Mokapi with custom base image": "resources/examples/mokapi-with-custom-base-image.md" - } + "items": [ + { + "label": "Mokapi behind Reverse Proxy", + "source": "resources/examples/mokapi-behind-proxy.md", + "path": "/resources/examples/mokapi-behind-proxy", + "hideNavigation": true + }, + { + "label": "Mokapi with custom base image", + "source": "resources/examples/mokapi-with-custom-base-image.md", + "path": "/resources/examples/mokapi-with-custom-base-image", + "hideNavigation": true + } + ] }, - "Blogs": { + { + "label": "Blogs", + "path": "/resources/blogs", "index": { "component": "examples", "hideNavigation": true, "hideInNavigation": true, - "canonical": "https://mokapi.io/docs/resources/blogs", + "canonical": "https://mokapi.io/resources/blogs", "title": "Explore Mokapi Resources: Tutorials, Examples, and Blog Articles", "description": "Explore Mokapi's resources including tutorials, examples, and blog articles. Learn to mock APIs, validate schemas, and streamline your development." }, - "items": { - "Ensuring API Contract Compliance with Mokapi": "resources/blogs/ensuring-api-contract-compliance-with-mokapi.md", - "Mock APIs based on OpenAPI and AsyncAPI": "resources/blogs/mock-api-based-on-openapi-asyncapi.md", - "Automation Testing in Agile Development": "resources/blogs/automation-testing-agile-development.md", - "Contract Testing": "resources/blogs/contract-testing.md", - "End to End Testing with Mocked APIs": "resources/blogs/end-to-end-testing-mocked-apis.md", - "Bring Your Mock APIs to Life with Mokapi and JavaScript": "resources/blogs/dynamic-mocks-with-javascript.md", - "Debugging Mokapi Scripts": "resources/blogs/debugging-mokapi-scripts.md", - "Acceptance Testing with Mokapi: Focus on What Matters": "resources/blogs/acceptance-testing.md", - "Testing Email Workflows with Playwright and Mokapi": "resources/blogs/testing-email-workflows-playwright.md", - "Testing Kafka Workflows with Playwright and Mokapi": "resources/blogs/testing-kafka-workflows-playwright.md" - } + "items": [ + { + "label": "Ensuring API Contract Compliance with Mokapi", + "source": "resources/blogs/ensuring-api-contract-compliance-with-mokapi.md", + "path": "/resources/blogs/ensuring-api-contract-compliance-with-mokapi", + "hideNavigation": true + }, + { + "label": "Mock APIs based on OpenAPI and AsyncAPI", + "source": "resources/blogs/mock-api-based-on-openapi-asyncapi.md", + "path": "/resources/blogs/mock-api-based-on-openapi-asyncapi", + "hideNavigation": true + }, + { + "label": "Automation Testing in Agile Development", + "source": "resources/blogs/automation-testing-agile-development.md", + "path": "/resources/blogs/automation-testing-agile-development", + "hideNavigation": true + }, + { + "label": "Contract Testing", + "source": "resources/blogs/contract-testing.md", + "path": "/resources/blogs/contract-testing", + "hideNavigation": true + }, + { + "label": "End to End Testing with Mocked APIs", + "source": "resources/blogs/end-to-end-testing-mocked-apis.md", + "path": "/resources/blogs/end-to-end-testing-with-mocked-apis", + "hideNavigation": true + }, + { + "label": "Bring Your Mock APIs to Life with Mokapi and JavaScript", + "source": "resources/blogs/dynamic-mocks-with-javascript.md", + "path": "/resources/blogs/dynamic-mocks-with-javascript", + "hideNavigation": true + }, + { + "label": "Debugging Mokapi Scripts", + "source": "resources/blogs/debugging-mokapi-scripts.md", + "path": "/resources/blogs/debugging-mokapi-scripts", + "hideNavigation": true + }, + { + "label": "Acceptance Testing with Mokapi: Focus on What Matters", + "source": "resources/blogs/acceptance-testing.md", + "path": "/resources/blogs/acceptance-testing", + "hideNavigation": true + }, + { + "label": "Testing Email Workflows with Playwright and Mokapi", + "source": "resources/blogs/testing-email-workflows-playwright.md", + "path": "/resources/blogs/testing-email-workflows-with-playwright-and-mokapi", + "hideNavigation": true + }, + { + "label": "Testing Kafka Workflows with Playwright and Mokapi", + "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 + } + ] } - } - }, - "References": { - "items": { - "Declarative Data": "references/declarative-data.md" - } + ] } } \ No newline at end of file diff --git a/docs/configuration/dynamic/file.md b/docs/configuration/dynamic/file.md index bb23f302b..bfbc0afd0 100644 --- a/docs/configuration/dynamic/file.md +++ b/docs/configuration/dynamic/file.md @@ -76,7 +76,10 @@ You can also define multiple file names or directory by separating them with sys ``` ### Include -One or more patterns that a file must match, except when empty. The filter is only applied to files. +A list of glob patterns that files must match to be processed. +- Applied only to files (not directories) +- If empty or omitted, all files are included +- If specified, a file must match at least one pattern to be included ```yaml tab=File (YAML) providers: @@ -90,6 +93,42 @@ providers: MOKAPI_PROVIDERS_FILE_INCLUDE="*.json *.yaml" ``` +### Exclude +A list of glob patterns that files must not match to be processed. +- Applied only to files (not directories) +- If empty or omitted, no files are excluded +- Exclusion is evaluated after include filtering +- If a file matches any exclude pattern, it is skipped + +```yaml tab=File (YAML) +providers: + file: + exclude: ["debug.json"] +``` +```bash tab=CLI +--providers-file-include debug.json +``` +```bash tab=Env +MOKAPI_PROVIDERS_FILE_INCLUDE="debug.json" +``` + +### Include + Exclude together + +When both are set: +1. Files are first filtered by include +2. Matching files are then filtered by exclude + +```yaml +providers: + file: + include: ["*.json"] + exclude: ["debug.json"] +``` + +- ✔ config.json → included +- ✖ debug.json → excluded +- ✖ config.yaml → not included + ### Ignoring Files and Directories You can create a `.mokapiignore` file in your directory to tell Mokapi which files and directories to ignore. The structure of this diff --git a/docs/configuration/introduction.md b/docs/configuration/introduction.md index 1f05b1c37..8175a3c88 100644 --- a/docs/configuration/introduction.md +++ b/docs/configuration/introduction.md @@ -2,7 +2,7 @@ title: Introduction to Mokapi Configuration | Static & Dynamic Setup Explained description: Discover how to configure Mokapi using static files or dynamic updates. Learn startup options, hot-reloading, and flexible setup for your mocked APIs. --- -# Introduction +# Configuration Mokapi has two types of configuration: - The startup configuration (referred as the static configuration) diff --git a/docs/configuration/reference.md b/docs/configuration/reference.md index 912e2d220..e146c648e 100644 --- a/docs/configuration/reference.md +++ b/docs/configuration/reference.md @@ -5,7 +5,7 @@ description: A complete list of all Mokapi options and how to set the option in # Complete Options Reference Options define Mokapi's run behavior that can be passed in multiple places. -Mokapi chooses the value from the [highest order of precedence](/docs/configuration/introduction.md). +Mokapi chooses the value from the [highest order of precedence](/docs/configuration/overview.md). ## Log Mokapi log level (default is info) diff --git a/docs/configuration/static/mokapi.md b/docs/configuration/static/mokapi.md new file mode 100644 index 000000000..273485d97 --- /dev/null +++ b/docs/configuration/static/mokapi.md @@ -0,0 +1,3 @@ +# Release Notes + +> ⚠️ This file is automatically generated during the release pipeline. \ No newline at end of file diff --git a/docs/references/declarative-data.md b/docs/data-generator/declarative-data.md similarity index 100% rename from docs/references/declarative-data.md rename to docs/data-generator/declarative-data.md diff --git a/docs/guides/get-started/dashboard.md b/docs/get-started/dashboard.md similarity index 100% rename from docs/guides/get-started/dashboard.md rename to docs/get-started/dashboard.md diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md new file mode 100644 index 000000000..f402e131e --- /dev/null +++ b/docs/get-started/installation.md @@ -0,0 +1,89 @@ +--- +title: "Install Mokapi: Quick & Easy Setup Guide" +description: Learn how to install Mokapi effortlessly across Windows, macOS, and Linux. Follow the step-by-step guide for a smooth setup experience. +--- +# Install Mokapi + +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. + +## Installation Options + +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 +``` + +### Direct Download + +Download the latest macOS version from [GitHub](https://github.com/marle3003/mokapi/releases) + +@tab "Windows" + +### Chocolatey + +```Powershell +choco install mokapi +``` + +### 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 + +```tab=deb +dpkg -i mokapi_{version}_linux_{arch}.deb +``` + +```tab=rpm +rpm -i mokapi_{version}_linux_{arch}.rpm +``` + +@tab "Docker" + +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](/resources/examples/mokapi-with-custom-base-image.md). + +``` +docker pull mokapi/mokapi +``` + +@tab "NPM" + +If you prefer to install Mokapi as a Node.js package, use the following command: + +```bash +npm install go-mokapi +``` + +::: + +### Mokapi Scripts Type Definitions + +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 + +- [Create your first Mock](/docs/get-started/running.md) +- [Install @types/mokapi](https://www.npmjs.com/package/@types/mokapi) \ No newline at end of file diff --git a/docs/guides/get-started/running.md b/docs/get-started/running.md similarity index 91% rename from docs/guides/get-started/running.md rename to docs/get-started/running.md index 57c65cbe2..43e6559fb 100644 --- a/docs/guides/get-started/running.md +++ b/docs/get-started/running.md @@ -6,7 +6,7 @@ description: Learn how to run your first mocked REST API using Mokapi. Monitor H This quick-start guide will help you set up Mokapi and mock an HTTP REST API using the [Swagger Petstore](https://swagger.io/) specification. If you haven’t installed -Mokapi yet, check out the [Installation Guide](/docs/guides/get-started/installation.md). +Mokapi yet, check out the [Installation Guide](/docs/get-started/installation.md). ## Mocking Petstore API Run the following command to start Mokapi using the Petstore OpenAPI specification directly from the web: @@ -56,7 +56,7 @@ You can analyze your request and the corresponding response by clicking on the l ## Next Steps -- [Test-Data](/docs/guides/get-started/test-data.md) -- [Dashboard](/docs/guides/get-started/dashboard.md) -- [Mocking HTTP API](/docs/guides/http) +- [Test-Data](/docs/get-started/test-data.md) +- [Dashboard](/docs/get-started/dashboard.md) +- [Mocking HTTP API](/docs/http/overview) diff --git a/docs/guides/get-started/test-data.md b/docs/get-started/test-data.md similarity index 96% rename from docs/guides/get-started/test-data.md rename to docs/get-started/test-data.md index 1c813667f..cb2c8ce23 100644 --- a/docs/guides/get-started/test-data.md +++ b/docs/get-started/test-data.md @@ -153,5 +153,4 @@ guide you through the process in more detail. ## Next Steps - [Using Mokapi's Dashboard](dashboard.md) -- [Explore Mokapi Scripts](../../javascript-api/overview.md) -- [Learn More About Declarative Test Data](../../references/declarative-data.md) +- [Explore Mokapi Scripts](../javascript-api/overview.md) diff --git a/docs/get-started/welcome.md b/docs/get-started/welcome.md new file mode 100644 index 000000000..9ed831374 --- /dev/null +++ b/docs/get-started/welcome.md @@ -0,0 +1,71 @@ +--- +title: "Getting Started with Mokapi" +description: Learn how to set up Mokapi to mock APIs and validate requests using OpenAPI or AsyncAPI. No account needed—free, open-source, and easy to use. +cards: + items: + - title: Run your first mocked REST API + href: /docs/get-started/running + description: Learn how to quickly set up and run your first mock REST API and view the results in the Mokapi dashboard. + - title: Using Mokapi + href: /docs/configuration/overview + description: Get an overview of Mokapi’s core features, configuration options, and how to patch and use configuration providers effectively. + - title: Mock Event-Driven APIs with Apache Kafka + href: /docs/kafka/quick-start + description: Learn how to mock Kafka topics and simulate event-driven architectures for realistic API testing. + - title: Mokapi JavaScript API + href: /docs/javascript-api/overview + description: Discover how to control and customize your mocked APIs programmatically with Mokapi’s JavaScript API. + - title: Random Data Generator + href: /docs/get-started/test-data + description: Explore Mokapi’s random data generator, and learn how to customize the data for your API testing needs. + - title: Mokapi Dashboard + href: /docs/get-started/dashboard + description: Dive into Mokapi’s dashboard to analyze and monitor APIs, requests, and responses in real time for efficient debugging. +--- + +# Mocking APIs with Mokapi + +**Welcome to Mokapi!** + +Mokapi is your go-to platform for mocking APIs, making it easy to build, +test, and monitor API-driven applications without the hassle. + +> *Think of Mokapi as your ever-reliable API contract guardian—lightweight, transparent, and specification-driven.* + +## Build Better Software with Mokapi + +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: + +- **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. + +## Key Features + +-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/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).
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.
-
-## Best Practices for Debugging
+### Dashboard Visualization
-- Use console.log freely during development to trace values and decisions inside your scripts.
-- Return true from important event handlers you want to trace in the dashboard.
-- Use console.error for anything unexpected to separate normal logs from problems.
-- Check the Dashboard for a clear overview of all event handlers triggered by a request.
+When handlers are tracked, the Mokapi dashboard displays detailed information for each request:
-## Conclusion
+
-Debugging Mokapi JavaScript is straightforward with console.log, console.error, and event handler
-tracing. Whether you're building complex mock logic or just getting started, these tools will help you
-understand exactly what happens inside Mokapi when it processes your requests.
+## Debug with Confidence
-Give it a try — and happy debugging!
\ No newline at end of file
+With console logging, event handler tracing, and strategic use of the track parameter, you have complete visibility
+into your Mokapi scripts. Whether you're building complex mock logic or troubleshooting production issues, these
+debugging tools will help you understand exactly what's happening at every step.
\ No newline at end of file
diff --git a/docs/resources/blogs/dynamic-mocks-with-javascript.md b/docs/resources/blogs/dynamic-mocks-with-javascript.md
index 1d4ecd9a3..03ca3e387 100644
--- a/docs/resources/blogs/dynamic-mocks-with-javascript.md
+++ b/docs/resources/blogs/dynamic-mocks-with-javascript.md
@@ -11,7 +11,7 @@ Mocking APIs is essential for fast development — but static mocks can quickly
become a bottleneck. Wouldn’t it be better if your mocks could think —
reacting to queries, headers, or even generating data on the fly?
-That's exactly what [Mokapi Scripts](/docs/javascript-api) are designed for.
+That's exactly what [Mokapi Scripts](/docs/javascript-api/overview.md) are designed for.
With just a few lines of JavaScript, you can control how your mocks behave —
making them dynamic, intelligent, and realistic.
@@ -132,9 +132,9 @@ demos and get faster feedback.
2. Add a Script where you control the response with JavaScript
3. Run Mokapi — that’s it!
-👉 Try the [OpenAPI Mocking Tutorial](/docs/resources/tutorials/get-started-with-rest-api) for a guided walkthrough.
+👉 Try the [OpenAPI Mocking Tutorial](/resources/tutorials/get-started-with-rest-api) for a guided walkthrough.
-👉 Check out the [Mokapi Installation Guide](/docs/guides/get-started/installation.md) to get set up in minutes.
+👉 Check out the [Mokapi Installation Guide](/docs/get-started/installation.md) to get set up in minutes.
## Conclusion
@@ -146,9 +146,9 @@ With Mokapi, your mocks behave exactly the way you need them to.
## Further Reading
-- [Debugging Mokapi JavaScript](/docs/resources/blogs/debugging-mokapi-scripts)\
+- [Debugging Mokapi JavaScript](/resources/blogs/debugging-mokapi-scripts)\
Learn how to debug your JavaScript code inside Mokapi
-- [End-to-End Testing with Mock APIs Using Mokapi](/docs/resources/blogs/end-to-end-testing-with-mocked-apis)\
+- [End-to-End Testing with Mock APIs Using Mokapi](/resources/blogs/end-to-end-testing-with-mocked-apis)\
Improve your end-to-end tests by mocking APIs with Mokapi.
---
diff --git a/docs/resources/blogs/end-to-end-testing-mocked-apis.md b/docs/resources/blogs/end-to-end-testing-mocked-apis.md
index c64100ce0..317a440bf 100644
--- a/docs/resources/blogs/end-to-end-testing-mocked-apis.md
+++ b/docs/resources/blogs/end-to-end-testing-mocked-apis.md
@@ -21,8 +21,8 @@ Mocking APIs allows you to simulate external systems under controlled conditions
- 🛠️ Easier testing of error scenarios, timeouts, and edge cases
- 🔗 No dependencies on the availability of third-party services
-With [Mokapi](https://mokapi.io), you can easily define API mocks using [OpenAPI](/docs/guides/http) or
-[AsyncAPI](/docs/guides/kafka) specifications, and serve them locally or in your CI environment.
+With [Mokapi](https://mokapi.io), you can easily define API mocks using [OpenAPI](/docs/http/overview.md) or
+[AsyncAPI](/docs/kafka/overview.md) specifications, and serve them locally or in your CI environment.
Mokapi even supports dynamic behavior using simple [JavaScripts](/docs/javascript-api/overview.md), helping you create more realistic test scenarios.
## How Mokapi Fits into Your CI/CD Pipeline
@@ -91,7 +91,7 @@ You can even modify mocks dynamically during a test run with simple [JavaScripts
## Local Development and Mokapi
Mokapi isn't just for CI. You can also run it locally during development.
-There are many ways to run Mokapi depending on your setup — learn more in the [Running Mokapi Guide](/docs/guides/get-started/running.md).
+There are many ways to run Mokapi depending on your setup — learn more in the [Running Mokapi Guide](/docs/get-started/running.md).
## Conclusion
@@ -99,10 +99,10 @@ Mocking APIs is a crucial part of building robust, scalable systems. With Mokapi
development and CI pipelines, leading to faster feedback, fewer bugs, and better products.
**Ready to ship faster, more reliable software?**
-[Get started with Mokapi](/docs/guides/get-started)
+[Get started with Mokapi](/docs/get-started/installation.md)
For a detailed, step-by-step guide on how to use Mokapi in your GitHub Actions workflows,
-see the [GitHub Actions and Mokapi](/docs/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline).
+see the [GitHub Actions and Mokapi](/resources/tutorials/running-mokapi-in-a-ci-cd-pipeline).
---
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..bf7e07aa3 100644
--- a/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md
+++ b/docs/resources/blogs/ensuring-api-contract-compliance-with-mokapi.md
@@ -4,6 +4,7 @@ description: Validate HTTP API requests and responses with Mokapi to catch break
image:
url: /mokapi-using-as-proxy.png
alt: Flow diagram illustrating how Mokapi enforces OpenAPI contracts between clients, Playwright tests, and backend APIs.
+tech: http
---
# Ensuring Compliance with the HTTP API Contract Using Mokapi for Request Forwarding and Validation
@@ -111,7 +112,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;
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/docs/resources/blogs/testing-email-workflows-playwright.md b/docs/resources/blogs/testing-email-workflows-playwright.md
index 55b8bc2c2..3313e79b1 100644
--- a/docs/resources/blogs/testing-email-workflows-playwright.md
+++ b/docs/resources/blogs/testing-email-workflows-playwright.md
@@ -168,4 +168,4 @@ Don’t leave your email experience to chance. Mock it, test it, and ship it wit
---
Try the example on GitHub: [mokapi-email-workflow](https://github.com/marle3003/mokapi-email-workflow)\
-Learn more about [Mokapi Mail](/docs/guides/mail)
\ No newline at end of file
+Learn more about [Mokapi Mail](/docs/mail/overview.md)
\ No newline at end of file
diff --git a/docs/resources/tutorials/mock-ldap-group-permission.md b/docs/resources/tutorials/mock-ldap-group-permission.md
index 2ed55c532..0409aedb4 100644
--- a/docs/resources/tutorials/mock-ldap-group-permission.md
+++ b/docs/resources/tutorials/mock-ldap-group-permission.md
@@ -25,7 +25,7 @@ LDAP server and implement group-based authentication in a Node.js backend.
Before starting, ensure you have the following:
- Node.js installed
-- Mokapi installed [Installation Guide](/docs/guides/get-started/installation.md)
+- Mokapi installed [Installation Guide](/docs/get-started/installation.md)
- Basic knowledge of LDAP authentication
## 1. Create an LDAP Mock Configuration
diff --git a/docs/resources/tutorials/mokapi-in-locale-tests.md b/docs/resources/tutorials/mokapi-in-locale-tests.md
index a70f0f656..abc58383c 100644
--- a/docs/resources/tutorials/mokapi-in-locale-tests.md
+++ b/docs/resources/tutorials/mokapi-in-locale-tests.md
@@ -26,7 +26,7 @@ brew install mokapi
choco install mokapi
```
-- On Linux: Follow the installation instructions [here](/docs/guides/get-started/installation.md).
+- On Linux: Follow the installation instructions [here](/docs/get-started/installation.md).
Once Mokapi is installed, you can start it from the command line:
diff --git a/docs/resources/tutorials/running-mokapi-github-action.md b/docs/resources/tutorials/running-mokapi-github-action.md
index e64ba5a1a..7fb8264c8 100644
--- a/docs/resources/tutorials/running-mokapi-github-action.md
+++ b/docs/resources/tutorials/running-mokapi-github-action.md
@@ -7,7 +7,7 @@ icon: bi-gear-wide-connected
Integrating Mokapi into a CI/CD pipeline ensures API contracts and Kafka topics interactions are validated before deployment.
This helps catch issues early, making the development process more reliable and reducing the risk of breaking changes in production.
-If you haven't installed Mokapi yet, follow the [Installation Guide](/docs/guides/get-started/installation.md) to get started.
+If you haven't installed Mokapi yet, follow the [Installation Guide](/docs/get-started/installation.md) to get started.
This guide explains how to:
diff --git a/engine/common/host.go b/engine/common/host.go
index e1df87c2e..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
@@ -49,6 +54,8 @@ type Host interface {
Unlock()
Store() Store
+
+ Cwd() string
}
type Logger interface {
diff --git a/engine/common/http.go b/engine/common/http.go
index 46db0412c..b9519e9d3 100644
--- a/engine/common/http.go
+++ b/engine/common/http.go
@@ -6,14 +6,16 @@ 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"`
+
+ Rebuild func(statusCode int, contentType string) `json:"-"`
}
-type EventRequest struct {
+type HttpEventRequest struct {
Method string `json:"method"`
Url Url `json:"url"`
Body interface{} `json:"body"`
@@ -36,7 +38,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 +63,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/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 2a72cdf3f..06c584d77 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"
)
@@ -27,10 +29,11 @@ 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
+ CwdFunc func() string
}
type HttpClient struct {
@@ -117,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)
}
}
@@ -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/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 6b8d21d93..e1e38b208 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 {
@@ -66,40 +67,10 @@ func (sh *scriptHost) Run() (err error) {
return
}
}
- log.Infof("executing script %v", sh.file.Info.Url)
+ log.Infof("executing script %v", sh.name)
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
}
@@ -284,8 +256,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 +318,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/engine/mokapi_on_test.go b/engine/mokapi_on_test.go
index 5b353f576..39b6b724b 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"
@@ -32,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
},
@@ -43,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"])
@@ -59,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
},
@@ -68,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)
},
@@ -83,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
},
@@ -92,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)
},
@@ -107,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)
@@ -131,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
},
@@ -140,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)
},
@@ -155,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)
@@ -180,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)
},
@@ -204,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
@@ -213,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))
},
@@ -228,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
@@ -237,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))
},
@@ -266,3 +267,114 @@ export default () => {
})
}
}
+
+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: "handlers in same script",
+ scripts: []string{`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.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)
+ 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) {
+ 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)
+ require.Equal(t, map[string]any{"foo": "handler3", "handler2": float64(0), "handler3": float64(1)}, mokapi.Export(res.Data))
+ _ = 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.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)
+ 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.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)
+ 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)
+
+ var opts []engine.Options
+ e := enginetest.NewEngine(opts...)
+ for i, s := range tc.scripts {
+ err := e.AddScript(newScript(fmt.Sprintf("test-%v.js", i), s))
+ require.NoError(t, err)
+ }
+
+ actions := tc.run(e)
+ tc.test(t, actions)
+ })
+ }
+}
diff --git a/examples/mokapi/services_http.js b/examples/mokapi/services_http.js
index 63b24e2e9..9c5bc401e 100644
--- a/examples/mokapi/services_http.js
+++ b/examples/mokapi/services_http.js
@@ -112,7 +112,8 @@ export const configs = {
data: 'http://localhost:8090/api/services/http/Swagger%20Petstore',
}
],
- filename: 'petstore.json'
+ filename: 'petstore.json',
+ tags: ['Tag1', 'Tag2'],
},
'b6fea8ac-56c7-4e73-a9c0-4487640bdca8': {
id: 'b6fea8ac-56c7-4e73-a9c0-4487640bdca8',
diff --git a/go.mod b/go.mod
index 1b03a8531..962b84143 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ 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.5
@@ -20,8 +20,8 @@ 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/text v0.33.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
layeh.com/gopher-luar v1.0.11
@@ -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 f3d599b06..6a4a75f21 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=
@@ -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,14 +207,14 @@ 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.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=
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
diff --git a/js/console/console.go b/js/console/console.go
index 847c846c6..af5e346a6 100644
--- a/js/console/console.go
+++ b/js/console/console.go
@@ -3,11 +3,12 @@ package console
import (
"encoding/json"
"fmt"
- "github.com/dop251/goja"
- "github.com/sirupsen/logrus"
"mokapi/engine/common"
"regexp"
"strings"
+
+ "github.com/dop251/goja"
+ "github.com/sirupsen/logrus"
)
type Module struct {
@@ -23,11 +24,11 @@ func Enable(vm *goja.Runtime) {
logger: host,
}
obj := vm.NewObject()
- obj.Set("log", f.Log)
- obj.Set("warn", f.Warn)
- obj.Set("error", f.Error)
- obj.Set("debug", f.Debug)
- vm.Set("console", obj)
+ _ = obj.Set("log", f.Log)
+ _ = obj.Set("warn", f.Warn)
+ _ = obj.Set("error", f.Error)
+ _ = obj.Set("debug", f.Debug)
+ _ = vm.Set("console", obj)
}
func (c *Module) Log(args ...goja.Value) {
@@ -47,37 +48,55 @@ func (c *Module) Debug(args ...goja.Value) {
}
func (c *Module) log(level logrus.Level, args ...goja.Value) {
- format := ""
- if len(args) > 0 {
- if s, ok := args[0].Export().(string); ok && strings.Contains(s, "%") {
- format = s
+ if len(args) == 0 {
+ return
+ }
+
+ var out []string
+
+ if format, ok := args[0].Export().(string); ok && strings.Contains(format, "%") {
+ specs := countFormatSpecifiers(format)
+
+ var exported []any
+ for _, arg := range args[1:] {
+ exported = append(exported, export(arg))
+ }
+
+ if specs > 0 && len(exported) >= specs {
+ if s, ok := formatString(format, exported[:specs]...); ok {
+ out = append(out, s)
+
+ // Append remaining args (browser behavior)
+ for _, arg := range exported[specs:] {
+ out = append(out, fmt.Sprintf("%v", arg))
+ }
+
+ goto LOG
+ }
}
}
- var logArgs []any
+ // Fallback: no formatting or formatting failed
for _, arg := range args {
- logArgs = append(logArgs, c.format(arg))
+ out = append(out, fmt.Sprintf("%v", export(arg)))
}
- if format != "" {
- if s, ok := formatString(format, logArgs[1:]...); ok {
- logArgs = []any{s}
- }
- }
+LOG:
+ msg := strings.Join(out, " ")
switch level {
case logrus.WarnLevel:
- c.logger.Warn(logArgs...)
+ c.logger.Warn(msg)
case logrus.ErrorLevel:
- c.logger.Error(logArgs...)
+ c.logger.Error(msg)
case logrus.DebugLevel:
- c.logger.Debug(logArgs...)
+ c.logger.Debug(msg)
default:
- c.logger.Info(logArgs...)
+ c.logger.Info(msg)
}
}
-func (c *Module) format(v goja.Value) any {
+func export(v goja.Value) any {
m, ok := v.(json.Marshaler)
if !ok {
return v.Export()
@@ -102,3 +121,23 @@ func formatString(format string, args ...any) (string, bool) {
return out, true
}
+
+func countFormatSpecifiers(format string) int {
+ count := 0
+ escaped := false
+
+ for i := 0; i < len(format); i++ {
+ if escaped {
+ escaped = false
+ continue
+ }
+ if format[i] == '%' {
+ if i+1 < len(format) && format[i+1] == '%' {
+ escaped = true
+ continue
+ }
+ count++
+ }
+ }
+ return count
+}
diff --git a/js/console/console_test.go b/js/console/console_test.go
index f42858f73..7c32af145 100644
--- a/js/console/console_test.go
+++ b/js/console/console_test.go
@@ -1,14 +1,15 @@
package console_test
import (
- "github.com/dop251/goja"
- r "github.com/stretchr/testify/require"
"mokapi/config/dynamic"
"mokapi/engine/enginetest"
"mokapi/js"
"mokapi/js/console"
"mokapi/js/eventloop"
"testing"
+
+ "github.com/dop251/goja"
+ r "github.com/stretchr/testify/require"
)
func TestConsole(t *testing.T) {
@@ -17,6 +18,21 @@ func TestConsole(t *testing.T) {
host *enginetest.Host
test func(t *testing.T, vm *goja.Runtime, host *enginetest.Host)
}{
+ {
+ name: "no parameters",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ var logs []any
+ host.InfoFunc = func(args ...interface{}) {
+ logs = args
+ }
+
+ _, err := vm.RunString(`
+ console.log();
+ `)
+ r.NoError(t, err)
+ r.Empty(t, logs)
+ },
+ },
{
name: "string",
test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
@@ -44,8 +60,7 @@ func TestConsole(t *testing.T) {
console.log('hello', 'world');
`)
r.NoError(t, err)
- r.Equal(t, "hello", logs[0])
- r.Equal(t, "world", logs[1])
+ r.Equal(t, "hello world", logs[0])
},
},
{
@@ -121,8 +136,7 @@ func TestConsole(t *testing.T) {
console.log('hello %', 123);
`)
r.NoError(t, err)
- r.Equal(t, `hello %`, logs[0])
- r.Equal(t, int64(123), logs[1])
+ r.Equal(t, `hello % 123`, logs[0])
},
},
{
@@ -170,6 +184,66 @@ func TestConsole(t *testing.T) {
r.Equal(t, `hello %9.2f`, logs[0])
},
},
+ {
+ name: "multiple parameters",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ var logs []any
+ host.InfoFunc = func(args ...interface{}) {
+ logs = args
+ }
+
+ _, err := vm.RunString(`
+ console.log('log an object:', { foo: 'bar', message: 'hello world' });
+ `)
+ r.NoError(t, err)
+ r.Equal(t, `log an object: {"foo":"bar","message":"hello world"}`, logs[0])
+ },
+ },
+ {
+ name: "log error",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ var logs []any
+ host.ErrorFunc = func(args ...interface{}) {
+ logs = args
+ }
+
+ _, err := vm.RunString(`
+ console.error('error');
+ `)
+ r.NoError(t, err)
+ r.Equal(t, "error", logs[0])
+ },
+ },
+ {
+ name: "log warn",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ var logs []any
+ host.WarnFunc = func(args ...interface{}) {
+ logs = args
+ }
+
+ _, err := vm.RunString(`
+ console.warn('warn');
+ `)
+ r.NoError(t, err)
+ r.Equal(t, "warn", logs[0])
+ },
+ },
+ {
+ name: "log debug",
+ test: func(t *testing.T, vm *goja.Runtime, host *enginetest.Host) {
+ var logs []any
+ host.DebugFunc = func(args ...interface{}) {
+ logs = args
+ }
+
+ _, err := vm.RunString(`
+ console.debug('debug');
+ `)
+ r.NoError(t, err)
+ r.Equal(t, "debug", logs[0])
+ },
+ },
}
for _, tc := range testcases {
diff --git a/js/file/file.go b/js/file/file.go
index b52e06a64..6c4312d97 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,87 @@ 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 {
+ return "", 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) 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 {
+ return fmt.Errorf("failed to write to file: %s", err)
+ }
+ defer func() {
+ _ = f.Close()
+ }()
+
+ _, err = f.WriteString(s)
+ if err != nil {
+ return fmt.Errorf("failed to write to file: %s", err)
+ }
+ return nil
+}
+
+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))
+ }
+ f, err := os.OpenFile(p,
+ os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ return fmt.Errorf("failed to append to file: %s", err)
+ }
+ defer func() {
+ _ = f.Close()
+ }()
+
+ _, err = f.WriteString(s)
+ if err != nil {
+ return fmt.Errorf("failed to append string: %v", err)
+ }
+ return nil
+}
+
+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..9be19d861 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,116 @@ 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: "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) {
+ 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: "write file with error",
+ 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('.', '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) {
+ 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 +210,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/mokapi/on.go b/js/mokapi/on.go
index 5e234a806..2fb2407ce 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
@@ -112,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)
}
@@ -131,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,
@@ -141,6 +151,13 @@ 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) {
+ case string, int, bool:
+ return p.vm.ToValue(val)
}
return vm.NewDynamicObject(p)
@@ -150,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 d9f0a9c5e..c61560a61 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
}
@@ -265,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
@@ -277,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"])
@@ -292,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
},
@@ -301,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)
},
@@ -315,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
},
@@ -324,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)
},
@@ -338,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