From 29f10e53ecef217a7cd8f8d003f4a2806d0be812 Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Wed, 13 May 2026 13:07:55 +0700 Subject: [PATCH 1/7] add -> wenowa. source from official: commitID: a4da7506ab0a7f69e63a30803dac4526ebac286f, github.com/Wenova-Co-LTD/wenova-microservices-go --- wenowa/DOCS.md | 170 +++++++++++++++++++++++++++++++++++++++ wenowa/address.go | 181 ++++++++++++++++++++++++++++++++++++++++++ wenowa/apiclient.go | 60 ++++++++++++++ wenowa/smsotp.go | 108 +++++++++++++++++++++++++ wenowa/wenowa.go | 3 + wenowa/wenowa_test.go | 53 +++++++++++++ 6 files changed, 575 insertions(+) create mode 100644 wenowa/DOCS.md create mode 100644 wenowa/address.go create mode 100644 wenowa/apiclient.go create mode 100644 wenowa/smsotp.go create mode 100644 wenowa/wenowa.go create mode 100644 wenowa/wenowa_test.go diff --git a/wenowa/DOCS.md b/wenowa/DOCS.md new file mode 100644 index 0000000..4780025 --- /dev/null +++ b/wenowa/DOCS.md @@ -0,0 +1,170 @@ +# wenowa + +`wenowa` provides Go helpers for calling Wenova microservice APIs from the `dextools` module. + +## Quick Start + +Use the package directly: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/barluscuda/dextools/wenowa" +) + +func main() { + ctx := context.Background() + client := wenowa.Wenowa{} + + otpResult, err := client.SendOtp(ctx, wenowa.SendOtpRequest{ + Header: "WNV-OTP", + PhoneNumber: "2012345678", + Message: "Code: 123456", + Token: "your-token", + UsePackage: true, + }) + if err != nil { + panic(err) + } + + provinces, err := client.GetProvinces(ctx, wenowa.Options{ + PluginKey: "your-plugin-key", + Lang: "en", + }) + if err != nil { + panic(err) + } + + fmt.Println(otpResult) + fmt.Println(provinces) +} +``` + +## Packages + +The package currently supports: + +- `SendOtp` +- `ScriptID` +- `GetProvinces` +- `GetProvinceById` +- `GetDistrictsByProvince` +- `GetDistrictById` +- `GetVillagesByDistrict` +- `GetVillageById` + +## Configuration + +All HTTP calls use the same base URL resolution rules: + +- an explicit `BaseURL` field wins when provided +- otherwise `WENOVA_API_URL` is used when set +- otherwise the default base URL is `https://apimicroservices.wenova.fun` + +Example environment value: + +```text +WENOVA_API_URL=https://apimicroservices.wenova.fun +``` + +## Structure + +Like `envtools`, `wenowa` uses a flat package structure: + +- `wenowa.go` defines the package type +- `apiclient.go` holds shared base URL and error helpers +- `smsotp.go` contains SMS and OTP helpers +- `address.go` contains address lookup helpers +- `wenowa_test.go` covers shared behavior + +You can call helpers either from package-level functions or from a `Wenowa` value. + +## SMS OTP + +`SendOtp` sends OTP and SMS package requests to `POST /sms/package`. + +### Request Rules + +`SendOtp` requires at least one of: + +- `Token` +- `ScriptID` + +If both are missing, it returns an error. + +### Example + +```go +result, err := wenowa.SendOtp(ctx, wenowa.SendOtpRequest{ + Header: "WNV-OTP", + PhoneNumber: "2012345678", + Message: "Code: 123456", + Token: "your-token", + UsePackage: true, +}) +``` + +### Helpers + +The package also includes: + +- `ScriptID(string) int64` for parsing a positive script ID from a string + +## Address + +The address helpers fetch Wenova Link location data for provinces, districts, and villages. + +### Options + +`Options` supports: + +- `PluginKey` for API access +- `KW` for keyword filtering +- `Lang` for response language +- `BaseURL` for overriding the API host + +### Available Functions + +- `GetProvinces` +- `GetProvinceById` +- `GetDistrictsByProvince` +- `GetDistrictById` +- `GetVillagesByDistrict` +- `GetVillageById` + +### Example + +```go +districts, err := wenowa.GetDistrictsByProvince(ctx, 1, wenowa.Options{ + PluginKey: "your-plugin-key", + KW: "chan", + Lang: "en", +}) +``` + +### Validation + +The address helpers enforce: + +- `PluginKey` must not be empty +- numeric IDs must be positive + +## Error Behavior + +Both request areas: + +- return decoded JSON as `any` on success +- return `nil, nil` for empty successful response bodies +- parse API error payloads and include the HTTP status code in returned errors + +## Testing + +Run all tests with: + +```bash +go test ./... +``` diff --git a/wenowa/address.go b/wenowa/address.go new file mode 100644 index 0000000..0cc7723 --- /dev/null +++ b/wenowa/address.go @@ -0,0 +1,181 @@ +package wenowa + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +// Options carries pluginKey and optional query flags. +type Options struct { + PluginKey string + KW string + BaseURL string + Lang string +} + +func assertPluginKey(key string) error { + if strings.TrimSpace(key) == "" { + return errors.New("pluginKey is required") + } + return nil +} + +func npmQuery(pluginKey string, opts Options) map[string]string { + q := map[string]string{"key": pluginKey} + if strings.TrimSpace(opts.KW) != "" { + q["kw"] = opts.KW + } + if strings.TrimSpace(opts.Lang) != "" { + q["lang"] = opts.Lang + } + return q +} + +func getJSON(ctx context.Context, url string, query map[string]string) (any, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + q := req.URL.Query() + for k, v := range query { + q.Set(k, v) + } + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, ErrorFromResponseBody(b, resp.StatusCode, "Request failed") + } + var out any + if len(b) == 0 { + return nil, nil + } + if err := json.Unmarshal(b, &out); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + return out, nil +} + +func assertPositiveID(name string, id int64) error { + if id <= 0 { + return fmt.Errorf("%s must be a positive number", name) + } + return nil +} + +// GetProvinces GET /npm-provinces?key=&kw=&lang= +func GetProvinces(ctx context.Context, opts Options) (any, error) { + return Wenowa{}.GetProvinces(ctx, opts) +} + +func (Wenowa) GetProvinces(ctx context.Context, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-provinces" + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} + +// GetProvinceById GET /npm-provinces/:id?key=&lang= +func GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { + return Wenowa{}.GetProvinceById(ctx, id, opts) +} + +func (Wenowa) GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + if err := assertPositiveID("id", id); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-provinces/" + strconv.FormatInt(id, 10) + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} + +// GetDistrictsByProvince GET /npm-districts/by-province/:provinceId?key=&kw=&lang= +func GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { + return Wenowa{}.GetDistrictsByProvince(ctx, provinceID, opts) +} + +func (Wenowa) GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + if err := assertPositiveID("provinceId", provinceID); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-districts/by-province/" + strconv.FormatInt(provinceID, 10) + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} + +// GetDistrictById GET /npm-districts/:id?key=&lang= +func GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { + return Wenowa{}.GetDistrictById(ctx, id, opts) +} + +func (Wenowa) GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + if err := assertPositiveID("id", id); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-districts/" + strconv.FormatInt(id, 10) + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} + +// GetVillagesByDistrict GET /npm-vilages/by-district/:districtId?key=&kw=&lang= +func GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { + return Wenowa{}.GetVillagesByDistrict(ctx, districtID, opts) +} + +func (Wenowa) GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + if err := assertPositiveID("districtId", districtID); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-vilages/by-district/" + strconv.FormatInt(districtID, 10) + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} + +// GetVillageById GET /npm-vilages/:id?key=&lang= +func GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { + return Wenowa{}.GetVillageById(ctx, id, opts) +} + +func (Wenowa) GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { + if err := assertPluginKey(opts.PluginKey); err != nil { + return nil, err + } + if err := assertPositiveID("id", id); err != nil { + return nil, err + } + base := ResolveBaseURL(opts.BaseURL) + url := base + "/npm-vilages/" + strconv.FormatInt(id, 10) + return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) +} diff --git a/wenowa/apiclient.go b/wenowa/apiclient.go new file mode 100644 index 0000000..c1d0330 --- /dev/null +++ b/wenowa/apiclient.go @@ -0,0 +1,60 @@ +package wenowa + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +const DefaultBaseURL = "https://apimicroservices.wenova.fun" + +// ResolveBaseURL returns override if set, else WENOVA_API_URL, else DefaultBaseURL. +func ResolveBaseURL(override string) string { + if strings.TrimSpace(override) != "" { + return strings.TrimSuffix(strings.TrimSpace(override), "/") + } + if v := strings.TrimSpace(os.Getenv("WENOVA_API_URL")); v != "" { + return strings.TrimSuffix(v, "/") + } + return strings.TrimSuffix(DefaultBaseURL, "/") +} + +// ResolveBaseURL returns override if set, else WENOVA_API_URL, else DefaultBaseURL. +func (Wenowa) ResolveBaseURL(override string) string { + return ResolveBaseURL(override) +} + +// ErrorFromResponseBody parses JSON error bodies like the Node SDKs. +func ErrorFromResponseBody(b []byte, status int, fallback string) error { + var wrap struct { + Message any `json:"message"` + Error any `json:"error"` + } + msg := fallback + if json.Unmarshal(b, &wrap) == nil { + if s, ok := stringifyErrPart(wrap.Message); ok && s != "" { + msg = s + } else if s, ok := stringifyErrPart(wrap.Error); ok && s != "" { + msg = s + } + } + return fmt.Errorf("%s (HTTP %d)", msg, status) +} + +func stringifyErrPart(v any) (string, bool) { + switch x := v.(type) { + case string: + return x, true + case map[string]any: + if m, ok := x["message"].(string); ok { + return m, true + } + if raw, err := json.Marshal(x); err == nil { + return string(raw), true + } + case float64, bool, json.Number: + return fmt.Sprint(x), true + } + return "", false +} diff --git a/wenowa/smsotp.go b/wenowa/smsotp.go new file mode 100644 index 0000000..ca0acad --- /dev/null +++ b/wenowa/smsotp.go @@ -0,0 +1,108 @@ +package wenowa + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +// SendOtpRequest matches the Node SDK payload. +type SendOtpRequest struct { + Header string + PhoneNumber string + Message string + Token string + ScriptID int64 + UsePackage bool + BaseURL string +} + +// SendOtp POSTs to /sms/package and returns the decoded JSON body. +func SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { + return Wenowa{}.SendOtp(ctx, req) +} + +func (Wenowa) SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { + hasToken := strings.TrimSpace(req.Token) != "" + hasScript := req.ScriptID > 0 + if !hasToken && !hasScript { + return nil, errors.New("either token or scriptId is required") + } + + base := ResolveBaseURL(req.BaseURL) + url := base + "/sms/package" + + body := map[string]any{ + "header": req.Header, + "phoneNumber": req.PhoneNumber, + "message": req.Message, + "usePackage": req.UsePackage, + } + if hasToken { + body["token"] = strings.TrimSpace(req.Token) + } + if hasScript { + body["scriptId"] = req.ScriptID + } + + raw, err := json.Marshal(body) + if err != nil { + return nil, err + } + + hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return nil, err + } + hreq.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(hreq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, ErrorFromResponseBody(b, resp.StatusCode, "Failed to send OTP") + } + + var out any + if len(b) == 0 { + return nil, nil + } + if err := json.Unmarshal(b, &out); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + return out, nil +} + +// ScriptID parses a positive script id from string. +func ScriptID(s string) int64 { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || n <= 0 { + return 0 + } + return n +} + +// ScriptID parses a positive script id from string. +func (Wenowa) ScriptID(s string) int64 { + return ScriptID(s) +} diff --git a/wenowa/wenowa.go b/wenowa/wenowa.go new file mode 100644 index 0000000..3d786b8 --- /dev/null +++ b/wenowa/wenowa.go @@ -0,0 +1,3 @@ +package wenowa + +type Wenowa struct{} diff --git a/wenowa/wenowa_test.go b/wenowa/wenowa_test.go new file mode 100644 index 0000000..aa3dcd3 --- /dev/null +++ b/wenowa/wenowa_test.go @@ -0,0 +1,53 @@ +package wenowa + +import ( + "strings" + "testing" +) + +func TestResolveBaseURL(t *testing.T) { + t.Setenv("WENOVA_API_URL", "") + if got := ResolveBaseURL(""); got != DefaultBaseURL { + t.Fatalf("expected default base url %q, got %q", DefaultBaseURL, got) + } + + t.Setenv("WENOVA_API_URL", "https://example.com/") + if got := ResolveBaseURL(""); got != "https://example.com" { + t.Fatalf("expected env base url to be trimmed, got %q", got) + } + + if got := ResolveBaseURL(" https://override.test/ "); got != "https://override.test" { + t.Fatalf("expected override base url to win, got %q", got) + } +} + +func TestScriptID(t *testing.T) { + tests := []struct { + input string + want int64 + }{ + {input: "", want: 0}, + {input: "abc", want: 0}, + {input: "-5", want: 0}, + {input: "42", want: 42}, + {input: " 8 ", want: 8}, + } + + for _, tt := range tests { + if got := ScriptID(tt.input); got != tt.want { + t.Fatalf("ScriptID(%q) = %d, want %d", tt.input, got, tt.want) + } + } +} + +func TestErrorFromResponseBody(t *testing.T) { + err := ErrorFromResponseBody([]byte(`{"message":"bad request"}`), 400, "fallback") + if err == nil || err.Error() != "bad request (HTTP 400)" { + t.Fatalf("unexpected error: %v", err) + } + + err = ErrorFromResponseBody([]byte(`not-json`), 500, "fallback") + if err == nil || !strings.Contains(err.Error(), "fallback (HTTP 500)") { + t.Fatalf("unexpected fallback error: %v", err) + } +} From 14d9cb6e26a3f07794a4476cd8426d4e333bd112 Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:08:09 +0700 Subject: [PATCH 2/7] add wenowa secrets to workflow --- .github/workflows/unit-tests.yml | 2 ++ README.md | 6 ++++++ wenowa/DOCS.md | 2 ++ wenowa/wenowa_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index aa0f305..6d75b55 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,4 +18,6 @@ jobs: go-version-file: go.mod - name: Run unit tests + env: + WENOWA_TOKEN: ${{ secrets.WENOWA_TOKEN }} run: go test ./... diff --git a/README.md b/README.md index 97edda3..3c03e30 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,9 @@ go test ./... ``` GitHub Actions is configured to run the test suite on `push` and `pull_request`. + +Some live integration tests read repository secrets through environment variables: + +- `MINECRAFT_MCSERVER_DEMO_IP` +- `MINECRAFT_MCSERVER_DEMO_PORT` +- `WENOWA_TOKEN` diff --git a/wenowa/DOCS.md b/wenowa/DOCS.md index 4780025..131bf22 100644 --- a/wenowa/DOCS.md +++ b/wenowa/DOCS.md @@ -168,3 +168,5 @@ Run all tests with: ```bash go test ./... ``` + +The live Wenowa integration test reads `WENOWA_TOKEN`. If it is not set, the test is skipped. In GitHub Actions, provide it as a repository secret with the same name. diff --git a/wenowa/wenowa_test.go b/wenowa/wenowa_test.go index aa3dcd3..0b21695 100644 --- a/wenowa/wenowa_test.go +++ b/wenowa/wenowa_test.go @@ -1,8 +1,11 @@ package wenowa import ( + "context" + "os" "strings" "testing" + "time" ) func TestResolveBaseURL(t *testing.T) { @@ -51,3 +54,24 @@ func TestErrorFromResponseBody(t *testing.T) { t.Fatalf("unexpected fallback error: %v", err) } } + +func TestGetProvincesLive(t *testing.T) { + token := strings.TrimSpace(os.Getenv("WENOWA_TOKEN")) + if token == "" { + t.Skip("skipping live Wenowa test: WENOWA_TOKEN is not set") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + result, err := GetProvinces(ctx, Options{ + PluginKey: token, + Lang: "en", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected province response, got nil") + } +} From c1ff9d8a4a1e6954c380b2abf30fff24633dc245 Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:14:19 +0700 Subject: [PATCH 3/7] add wenova api test --- .github/workflows/unit-tests.yml | 3 +++ README.md | 3 +++ wenowa/DOCS.md | 11 ++++++++- wenowa/wenowa_test.go | 40 ++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6d75b55..b4a58b1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,4 +20,7 @@ jobs: - name: Run unit tests env: WENOWA_TOKEN: ${{ secrets.WENOWA_TOKEN }} + WENOWA_DEMO_PHONE_NUMBER: ${{ secrets.WENOWA_DEMO_PHONE_NUMBER }} + WENOWA_DEMO_HEADER: ${{ secrets.WENOWA_DEMO_HEADER }} + WENOWA_DEMO_MESSAGE: ${{ secrets.WENOWA_DEMO_MESSAGE }} run: go test ./... diff --git a/README.md b/README.md index 3c03e30..363ba78 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,6 @@ Some live integration tests read repository secrets through environment variable - `MINECRAFT_MCSERVER_DEMO_IP` - `MINECRAFT_MCSERVER_DEMO_PORT` - `WENOWA_TOKEN` +- `WENOWA_DEMO_PHONE_NUMBER` +- `WENOWA_DEMO_HEADER` +- `WENOWA_DEMO_MESSAGE` diff --git a/wenowa/DOCS.md b/wenowa/DOCS.md index 131bf22..c89e5bb 100644 --- a/wenowa/DOCS.md +++ b/wenowa/DOCS.md @@ -169,4 +169,13 @@ Run all tests with: go test ./... ``` -The live Wenowa integration test reads `WENOWA_TOKEN`. If it is not set, the test is skipped. In GitHub Actions, provide it as a repository secret with the same name. +The live Wenowa address test reads `WENOWA_TOKEN`. If it is not set, the test is skipped. + +The live Wenowa service API test reads: + +- `WENOWA_TOKEN` +- `WENOWA_DEMO_PHONE_NUMBER` +- `WENOWA_DEMO_HEADER` (optional) +- `WENOWA_DEMO_MESSAGE` (optional) + +If `WENOWA_TOKEN` or `WENOWA_DEMO_PHONE_NUMBER` is missing, the service test is skipped. In GitHub Actions, provide these values as repository secrets with the same names. diff --git a/wenowa/wenowa_test.go b/wenowa/wenowa_test.go index 0b21695..04dc868 100644 --- a/wenowa/wenowa_test.go +++ b/wenowa/wenowa_test.go @@ -2,6 +2,7 @@ package wenowa import ( "context" + "fmt" "os" "strings" "testing" @@ -75,3 +76,42 @@ func TestGetProvincesLive(t *testing.T) { t.Fatal("expected province response, got nil") } } + +func TestSendOtpLive(t *testing.T) { + token := strings.TrimSpace(os.Getenv("WENOWA_TOKEN")) + if token == "" { + t.Skip("skipping live Wenowa service test: WENOWA_TOKEN is not set") + } + + phoneNumber := strings.TrimSpace(os.Getenv("WENOWA_DEMO_PHONE_NUMBER")) + if phoneNumber == "" { + t.Skip("skipping live Wenowa service test: WENOWA_DEMO_PHONE_NUMBER is not set") + } + + header := strings.TrimSpace(os.Getenv("WENOWA_DEMO_HEADER")) + if header == "" { + header = "WNV-OTP" + } + + message := strings.TrimSpace(os.Getenv("WENOWA_DEMO_MESSAGE")) + if message == "" { + message = fmt.Sprintf("Code: %d", time.Now().Unix()%1000000) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + result, err := SendOtp(ctx, SendOtpRequest{ + Header: header, + PhoneNumber: phoneNumber, + Message: message, + Token: token, + UsePackage: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected OTP response, got nil") + } +} From 4e56834824492e6c7b03a63eb23c62b4ea777c13 Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:29:37 +0700 Subject: [PATCH 4/7] rename from wenowa to wenova --- .github/workflows/unit-tests.yml | 9 +++-- README.md | 9 +++-- {wenowa => wenova}/DOCS.md | 38 +++++++++---------- {wenowa => wenova}/address.go | 26 ++++++------- {wenowa => wenova}/apiclient.go | 4 +- {wenowa => wenova}/smsotp.go | 8 ++-- wenova/wenova.go | 3 ++ .../wenowa_test.go => wenova/wenova_test.go | 26 +++++++------ wenowa/wenowa.go | 3 -- 9 files changed, 66 insertions(+), 60 deletions(-) rename {wenowa => wenova}/DOCS.md (76%) rename {wenowa => wenova}/address.go (86%) rename {wenowa => wenova}/apiclient.go (95%) rename {wenowa => wenova}/smsotp.go (93%) create mode 100644 wenova/wenova.go rename wenowa/wenowa_test.go => wenova/wenova_test.go (79%) delete mode 100644 wenowa/wenowa.go diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index b4a58b1..77f189a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -19,8 +19,9 @@ jobs: - name: Run unit tests env: - WENOWA_TOKEN: ${{ secrets.WENOWA_TOKEN }} - WENOWA_DEMO_PHONE_NUMBER: ${{ secrets.WENOWA_DEMO_PHONE_NUMBER }} - WENOWA_DEMO_HEADER: ${{ secrets.WENOWA_DEMO_HEADER }} - WENOWA_DEMO_MESSAGE: ${{ secrets.WENOWA_DEMO_MESSAGE }} + WENOVA_PLUGIN_KEY: ${{ secrets.WENOVA_PLUGIN_KEY }} + WENOVA_TOKEN: ${{ secrets.WENOVA_TOKEN }} + WENOVA_DEMO_PHONE_NUMBER: ${{ secrets.WENOVA_DEMO_PHONE_NUMBER }} + WENOVA_DEMO_HEADER: ${{ secrets.WENOVA_DEMO_HEADER }} + WENOVA_DEMO_MESSAGE: ${{ secrets.WENOVA_DEMO_MESSAGE }} run: go test ./... diff --git a/README.md b/README.md index 363ba78..37fcfe5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ Some live integration tests read repository secrets through environment variable - `MINECRAFT_MCSERVER_DEMO_IP` - `MINECRAFT_MCSERVER_DEMO_PORT` -- `WENOWA_TOKEN` -- `WENOWA_DEMO_PHONE_NUMBER` -- `WENOWA_DEMO_HEADER` -- `WENOWA_DEMO_MESSAGE` +- `WENOVA_PLUGIN_KEY` +- `WENOVA_TOKEN` +- `WENOVA_DEMO_PHONE_NUMBER` +- `WENOVA_DEMO_HEADER` +- `WENOVA_DEMO_MESSAGE` diff --git a/wenowa/DOCS.md b/wenova/DOCS.md similarity index 76% rename from wenowa/DOCS.md rename to wenova/DOCS.md index c89e5bb..78a06fc 100644 --- a/wenowa/DOCS.md +++ b/wenova/DOCS.md @@ -1,6 +1,6 @@ -# wenowa +# wenova -`wenowa` provides Go helpers for calling Wenova microservice APIs from the `dextools` module. +`wenova` provides Go helpers for calling Wenova microservice APIs from the `dextools` module. ## Quick Start @@ -13,14 +13,14 @@ import ( "context" "fmt" - "github.com/barluscuda/dextools/wenowa" + "github.com/barluscuda/dextools/wenova" ) func main() { ctx := context.Background() - client := wenowa.Wenowa{} + client := wenova.Wenova{} - otpResult, err := client.SendOtp(ctx, wenowa.SendOtpRequest{ + otpResult, err := client.SendOtp(ctx, wenova.SendOtpRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", @@ -31,7 +31,7 @@ func main() { panic(err) } - provinces, err := client.GetProvinces(ctx, wenowa.Options{ + provinces, err := client.GetProvinces(ctx, wenova.Options{ PluginKey: "your-plugin-key", Lang: "en", }) @@ -73,15 +73,15 @@ WENOVA_API_URL=https://apimicroservices.wenova.fun ## Structure -Like `envtools`, `wenowa` uses a flat package structure: +Like `envtools`, `wenova` uses a flat package structure: -- `wenowa.go` defines the package type +- `wenova.go` defines the package type - `apiclient.go` holds shared base URL and error helpers - `smsotp.go` contains SMS and OTP helpers - `address.go` contains address lookup helpers -- `wenowa_test.go` covers shared behavior +- `wenova_test.go` covers shared behavior -You can call helpers either from package-level functions or from a `Wenowa` value. +You can call helpers either from package-level functions or from a `Wenova` value. ## SMS OTP @@ -99,7 +99,7 @@ If both are missing, it returns an error. ### Example ```go -result, err := wenowa.SendOtp(ctx, wenowa.SendOtpRequest{ +result, err := wenova.SendOtp(ctx, wenova.SendOtpRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", @@ -139,7 +139,7 @@ The address helpers fetch Wenova Link location data for provinces, districts, an ### Example ```go -districts, err := wenowa.GetDistrictsByProvince(ctx, 1, wenowa.Options{ +districts, err := wenova.GetDistrictsByProvince(ctx, 1, wenova.Options{ PluginKey: "your-plugin-key", KW: "chan", Lang: "en", @@ -169,13 +169,13 @@ Run all tests with: go test ./... ``` -The live Wenowa address test reads `WENOWA_TOKEN`. If it is not set, the test is skipped. +The live Wenova address test reads `WENOVA_PLUGIN_KEY`. If it is not set, the test is skipped. -The live Wenowa service API test reads: +The live Wenova service API test reads: -- `WENOWA_TOKEN` -- `WENOWA_DEMO_PHONE_NUMBER` -- `WENOWA_DEMO_HEADER` (optional) -- `WENOWA_DEMO_MESSAGE` (optional) +- `WENOVA_TOKEN` +- `WENOVA_DEMO_PHONE_NUMBER` +- `WENOVA_DEMO_HEADER` (optional) +- `WENOVA_DEMO_MESSAGE` (optional) -If `WENOWA_TOKEN` or `WENOWA_DEMO_PHONE_NUMBER` is missing, the service test is skipped. In GitHub Actions, provide these values as repository secrets with the same names. +If `WENOVA_TOKEN` or `WENOVA_DEMO_PHONE_NUMBER` is missing, the service test is skipped. In GitHub Actions, provide these values as repository secrets with the same names. diff --git a/wenowa/address.go b/wenova/address.go similarity index 86% rename from wenowa/address.go rename to wenova/address.go index 0cc7723..b10d32b 100644 --- a/wenowa/address.go +++ b/wenova/address.go @@ -1,4 +1,4 @@ -package wenowa +package wenova import ( "context" @@ -83,10 +83,10 @@ func assertPositiveID(name string, id int64) error { // GetProvinces GET /npm-provinces?key=&kw=&lang= func GetProvinces(ctx context.Context, opts Options) (any, error) { - return Wenowa{}.GetProvinces(ctx, opts) + return Wenova{}.GetProvinces(ctx, opts) } -func (Wenowa) GetProvinces(ctx context.Context, opts Options) (any, error) { +func (Wenova) GetProvinces(ctx context.Context, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } @@ -97,10 +97,10 @@ func (Wenowa) GetProvinces(ctx context.Context, opts Options) (any, error) { // GetProvinceById GET /npm-provinces/:id?key=&lang= func GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenowa{}.GetProvinceById(ctx, id, opts) + return Wenova{}.GetProvinceById(ctx, id, opts) } -func (Wenowa) GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { +func (Wenova) GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } @@ -114,10 +114,10 @@ func (Wenowa) GetProvinceById(ctx context.Context, id int64, opts Options) (any, // GetDistrictsByProvince GET /npm-districts/by-province/:provinceId?key=&kw=&lang= func GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { - return Wenowa{}.GetDistrictsByProvince(ctx, provinceID, opts) + return Wenova{}.GetDistrictsByProvince(ctx, provinceID, opts) } -func (Wenowa) GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { +func (Wenova) GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } @@ -131,10 +131,10 @@ func (Wenowa) GetDistrictsByProvince(ctx context.Context, provinceID int64, opts // GetDistrictById GET /npm-districts/:id?key=&lang= func GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenowa{}.GetDistrictById(ctx, id, opts) + return Wenova{}.GetDistrictById(ctx, id, opts) } -func (Wenowa) GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { +func (Wenova) GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } @@ -148,10 +148,10 @@ func (Wenowa) GetDistrictById(ctx context.Context, id int64, opts Options) (any, // GetVillagesByDistrict GET /npm-vilages/by-district/:districtId?key=&kw=&lang= func GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { - return Wenowa{}.GetVillagesByDistrict(ctx, districtID, opts) + return Wenova{}.GetVillagesByDistrict(ctx, districtID, opts) } -func (Wenowa) GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { +func (Wenova) GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } @@ -165,10 +165,10 @@ func (Wenowa) GetVillagesByDistrict(ctx context.Context, districtID int64, opts // GetVillageById GET /npm-vilages/:id?key=&lang= func GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenowa{}.GetVillageById(ctx, id, opts) + return Wenova{}.GetVillageById(ctx, id, opts) } -func (Wenowa) GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { +func (Wenova) GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { if err := assertPluginKey(opts.PluginKey); err != nil { return nil, err } diff --git a/wenowa/apiclient.go b/wenova/apiclient.go similarity index 95% rename from wenowa/apiclient.go rename to wenova/apiclient.go index c1d0330..11358bc 100644 --- a/wenowa/apiclient.go +++ b/wenova/apiclient.go @@ -1,4 +1,4 @@ -package wenowa +package wenova import ( "encoding/json" @@ -21,7 +21,7 @@ func ResolveBaseURL(override string) string { } // ResolveBaseURL returns override if set, else WENOVA_API_URL, else DefaultBaseURL. -func (Wenowa) ResolveBaseURL(override string) string { +func (Wenova) ResolveBaseURL(override string) string { return ResolveBaseURL(override) } diff --git a/wenowa/smsotp.go b/wenova/smsotp.go similarity index 93% rename from wenowa/smsotp.go rename to wenova/smsotp.go index ca0acad..11f5974 100644 --- a/wenowa/smsotp.go +++ b/wenova/smsotp.go @@ -1,4 +1,4 @@ -package wenowa +package wenova import ( "bytes" @@ -26,10 +26,10 @@ type SendOtpRequest struct { // SendOtp POSTs to /sms/package and returns the decoded JSON body. func SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { - return Wenowa{}.SendOtp(ctx, req) + return Wenova{}.SendOtp(ctx, req) } -func (Wenowa) SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { +func (Wenova) SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { hasToken := strings.TrimSpace(req.Token) != "" hasScript := req.ScriptID > 0 if !hasToken && !hasScript { @@ -103,6 +103,6 @@ func ScriptID(s string) int64 { } // ScriptID parses a positive script id from string. -func (Wenowa) ScriptID(s string) int64 { +func (Wenova) ScriptID(s string) int64 { return ScriptID(s) } diff --git a/wenova/wenova.go b/wenova/wenova.go new file mode 100644 index 0000000..8d4e669 --- /dev/null +++ b/wenova/wenova.go @@ -0,0 +1,3 @@ +package wenova + +type Wenova struct{} diff --git a/wenowa/wenowa_test.go b/wenova/wenova_test.go similarity index 79% rename from wenowa/wenowa_test.go rename to wenova/wenova_test.go index 04dc868..4392ae3 100644 --- a/wenowa/wenowa_test.go +++ b/wenova/wenova_test.go @@ -1,4 +1,4 @@ -package wenowa +package wenova import ( "context" @@ -9,6 +9,10 @@ import ( "time" ) +func requiredTrimmedEnv(key string) string { + return strings.TrimSpace(os.Getenv(key)) +} + func TestResolveBaseURL(t *testing.T) { t.Setenv("WENOVA_API_URL", "") if got := ResolveBaseURL(""); got != DefaultBaseURL { @@ -57,16 +61,16 @@ func TestErrorFromResponseBody(t *testing.T) { } func TestGetProvincesLive(t *testing.T) { - token := strings.TrimSpace(os.Getenv("WENOWA_TOKEN")) - if token == "" { - t.Skip("skipping live Wenowa test: WENOWA_TOKEN is not set") + pluginKey := requiredTrimmedEnv("WENOVA_PLUGIN_KEY") + if pluginKey == "" { + t.Skip("skipping live Wenova address test: WENOVA_PLUGIN_KEY is not set") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() result, err := GetProvinces(ctx, Options{ - PluginKey: token, + PluginKey: pluginKey, Lang: "en", }) if err != nil { @@ -78,22 +82,22 @@ func TestGetProvincesLive(t *testing.T) { } func TestSendOtpLive(t *testing.T) { - token := strings.TrimSpace(os.Getenv("WENOWA_TOKEN")) + token := requiredTrimmedEnv("WENOVA_TOKEN") if token == "" { - t.Skip("skipping live Wenowa service test: WENOWA_TOKEN is not set") + t.Skip("skipping live Wenova service test: WENOVA_TOKEN is not set") } - phoneNumber := strings.TrimSpace(os.Getenv("WENOWA_DEMO_PHONE_NUMBER")) + phoneNumber := requiredTrimmedEnv("WENOVA_DEMO_PHONE_NUMBER") if phoneNumber == "" { - t.Skip("skipping live Wenowa service test: WENOWA_DEMO_PHONE_NUMBER is not set") + t.Skip("skipping live Wenova service test: WENOVA_DEMO_PHONE_NUMBER is not set") } - header := strings.TrimSpace(os.Getenv("WENOWA_DEMO_HEADER")) + header := requiredTrimmedEnv("WENOVA_DEMO_HEADER") if header == "" { header = "WNV-OTP" } - message := strings.TrimSpace(os.Getenv("WENOWA_DEMO_MESSAGE")) + message := requiredTrimmedEnv("WENOVA_DEMO_MESSAGE") if message == "" { message = fmt.Sprintf("Code: %d", time.Now().Unix()%1000000) } diff --git a/wenowa/wenowa.go b/wenowa/wenowa.go deleted file mode 100644 index 3d786b8..0000000 --- a/wenowa/wenowa.go +++ /dev/null @@ -1,3 +0,0 @@ -package wenowa - -type Wenowa struct{} From 470c222c4ef2a07a799fdb4037cebd523e2252e1 Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:33:03 +0700 Subject: [PATCH 5/7] fix wenova package not founs --- .github/workflows/unit-tests.yml | 2 ++ README.md | 2 ++ wenova/DOCS.md | 4 +++- wenova/wenova_test.go | 19 +++++++++++++------ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 77f189a..d11b1d1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -21,7 +21,9 @@ jobs: env: WENOVA_PLUGIN_KEY: ${{ secrets.WENOVA_PLUGIN_KEY }} WENOVA_TOKEN: ${{ secrets.WENOVA_TOKEN }} + WENOVA_SCRIPT_ID: ${{ secrets.WENOVA_SCRIPT_ID }} WENOVA_DEMO_PHONE_NUMBER: ${{ secrets.WENOVA_DEMO_PHONE_NUMBER }} WENOVA_DEMO_HEADER: ${{ secrets.WENOVA_DEMO_HEADER }} WENOVA_DEMO_MESSAGE: ${{ secrets.WENOVA_DEMO_MESSAGE }} + WENOVA_DEMO_USE_PACKAGE: ${{ secrets.WENOVA_DEMO_USE_PACKAGE }} run: go test ./... diff --git a/README.md b/README.md index 37fcfe5..0c8b3f9 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Some live integration tests read repository secrets through environment variable - `MINECRAFT_MCSERVER_DEMO_PORT` - `WENOVA_PLUGIN_KEY` - `WENOVA_TOKEN` +- `WENOVA_SCRIPT_ID` - `WENOVA_DEMO_PHONE_NUMBER` - `WENOVA_DEMO_HEADER` - `WENOVA_DEMO_MESSAGE` +- `WENOVA_DEMO_USE_PACKAGE` diff --git a/wenova/DOCS.md b/wenova/DOCS.md index 78a06fc..4d6f195 100644 --- a/wenova/DOCS.md +++ b/wenova/DOCS.md @@ -174,8 +174,10 @@ The live Wenova address test reads `WENOVA_PLUGIN_KEY`. If it is not set, the te The live Wenova service API test reads: - `WENOVA_TOKEN` +- `WENOVA_SCRIPT_ID` - `WENOVA_DEMO_PHONE_NUMBER` - `WENOVA_DEMO_HEADER` (optional) - `WENOVA_DEMO_MESSAGE` (optional) +- `WENOVA_DEMO_USE_PACKAGE` (optional) -If `WENOVA_TOKEN` or `WENOVA_DEMO_PHONE_NUMBER` is missing, the service test is skipped. In GitHub Actions, provide these values as repository secrets with the same names. +The service test requires `WENOVA_DEMO_PHONE_NUMBER` and at least one of `WENOVA_TOKEN` or `WENOVA_SCRIPT_ID`. `WENOVA_DEMO_USE_PACKAGE` defaults to `false` and should only be enabled when the credential is linked to a valid package. In GitHub Actions, provide these values as repository secrets with the same names. diff --git a/wenova/wenova_test.go b/wenova/wenova_test.go index 4392ae3..60af01b 100644 --- a/wenova/wenova_test.go +++ b/wenova/wenova_test.go @@ -13,6 +13,11 @@ func requiredTrimmedEnv(key string) string { return strings.TrimSpace(os.Getenv(key)) } +func optionalBoolEnv(key string) bool { + v := strings.ToLower(requiredTrimmedEnv(key)) + return v == "1" || v == "true" || v == "yes" +} + func TestResolveBaseURL(t *testing.T) { t.Setenv("WENOVA_API_URL", "") if got := ResolveBaseURL(""); got != DefaultBaseURL { @@ -82,16 +87,17 @@ func TestGetProvincesLive(t *testing.T) { } func TestSendOtpLive(t *testing.T) { - token := requiredTrimmedEnv("WENOVA_TOKEN") - if token == "" { - t.Skip("skipping live Wenova service test: WENOVA_TOKEN is not set") - } - phoneNumber := requiredTrimmedEnv("WENOVA_DEMO_PHONE_NUMBER") if phoneNumber == "" { t.Skip("skipping live Wenova service test: WENOVA_DEMO_PHONE_NUMBER is not set") } + token := requiredTrimmedEnv("WENOVA_TOKEN") + scriptID := ScriptID(requiredTrimmedEnv("WENOVA_SCRIPT_ID")) + if token == "" && scriptID == 0 { + t.Skip("skipping live Wenova service test: WENOVA_TOKEN or WENOVA_SCRIPT_ID is required") + } + header := requiredTrimmedEnv("WENOVA_DEMO_HEADER") if header == "" { header = "WNV-OTP" @@ -110,7 +116,8 @@ func TestSendOtpLive(t *testing.T) { PhoneNumber: phoneNumber, Message: message, Token: token, - UsePackage: true, + ScriptID: scriptID, + UsePackage: optionalBoolEnv("WENOVA_DEMO_USE_PACKAGE"), }) if err != nil { t.Fatalf("unexpected error: %v", err) From 1c4b90a2488dc6eedfe0e733ef4db11cd1117c8e Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:48:52 +0700 Subject: [PATCH 6/7] add wenova sms final code --- .github/workflows/unit-tests.yml | 3 - README.md | 3 - wenova/DOCS.md | 112 +++---------------- wenova/address.go | 181 ------------------------------- wenova/apiclient.go | 60 ---------- wenova/sms.go | 115 ++++++++++++++++++++ wenova/smsotp.go | 108 ------------------ wenova/wenova.go | 6 +- wenova/wenova_test.go | 77 ++----------- 9 files changed, 144 insertions(+), 521 deletions(-) delete mode 100644 wenova/address.go delete mode 100644 wenova/apiclient.go create mode 100644 wenova/sms.go delete mode 100644 wenova/smsotp.go diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d11b1d1..120f4c1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -19,11 +19,8 @@ jobs: - name: Run unit tests env: - WENOVA_PLUGIN_KEY: ${{ secrets.WENOVA_PLUGIN_KEY }} WENOVA_TOKEN: ${{ secrets.WENOVA_TOKEN }} - WENOVA_SCRIPT_ID: ${{ secrets.WENOVA_SCRIPT_ID }} WENOVA_DEMO_PHONE_NUMBER: ${{ secrets.WENOVA_DEMO_PHONE_NUMBER }} WENOVA_DEMO_HEADER: ${{ secrets.WENOVA_DEMO_HEADER }} WENOVA_DEMO_MESSAGE: ${{ secrets.WENOVA_DEMO_MESSAGE }} - WENOVA_DEMO_USE_PACKAGE: ${{ secrets.WENOVA_DEMO_USE_PACKAGE }} run: go test ./... diff --git a/README.md b/README.md index 0c8b3f9..2e92123 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,7 @@ Some live integration tests read repository secrets through environment variable - `MINECRAFT_MCSERVER_DEMO_IP` - `MINECRAFT_MCSERVER_DEMO_PORT` -- `WENOVA_PLUGIN_KEY` - `WENOVA_TOKEN` -- `WENOVA_SCRIPT_ID` - `WENOVA_DEMO_PHONE_NUMBER` - `WENOVA_DEMO_HEADER` - `WENOVA_DEMO_MESSAGE` -- `WENOVA_DEMO_USE_PACKAGE` diff --git a/wenova/DOCS.md b/wenova/DOCS.md index 4d6f195..e57d126 100644 --- a/wenova/DOCS.md +++ b/wenova/DOCS.md @@ -18,29 +18,21 @@ import ( func main() { ctx := context.Background() - client := wenova.Wenova{} + client := wenova.Wenova{ + BaseUrl: "https://apimicroservices.wenova.fun", + } - otpResult, err := client.SendOtp(ctx, wenova.SendOtpRequest{ + smsResult, err := client.SendSMS(ctx, wenova.SendSMSRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", Token: "your-token", - UsePackage: true, }) if err != nil { panic(err) } - provinces, err := client.GetProvinces(ctx, wenova.Options{ - PluginKey: "your-plugin-key", - Lang: "en", - }) - if err != nil { - panic(err) - } - - fmt.Println(otpResult) - fmt.Println(provinces) + fmt.Println(smsResult) } ``` @@ -48,115 +40,43 @@ func main() { The package currently supports: -- `SendOtp` -- `ScriptID` -- `GetProvinces` -- `GetProvinceById` -- `GetDistrictsByProvince` -- `GetDistrictById` -- `GetVillagesByDistrict` -- `GetVillageById` +- `SendSMS` ## Configuration -All HTTP calls use the same base URL resolution rules: - -- an explicit `BaseURL` field wins when provided -- otherwise `WENOVA_API_URL` is used when set -- otherwise the default base URL is `https://apimicroservices.wenova.fun` - -Example environment value: - -```text -WENOVA_API_URL=https://apimicroservices.wenova.fun -``` +Set `Wenova.BaseUrl` to customize the API host. If it is empty, the default base URL is `https://apimicroservices.wenova.fun`. ## Structure Like `envtools`, `wenova` uses a flat package structure: - `wenova.go` defines the package type -- `apiclient.go` holds shared base URL and error helpers -- `smsotp.go` contains SMS and OTP helpers -- `address.go` contains address lookup helpers +- `sms.go` contains SMS helpers - `wenova_test.go` covers shared behavior You can call helpers either from package-level functions or from a `Wenova` value. -## SMS OTP +## SMS -`SendOtp` sends OTP and SMS package requests to `POST /sms/package`. +`SendSMS` sends SMS requests to `POST /sms/package`. ### Request Rules -`SendOtp` requires at least one of: - -- `Token` -- `ScriptID` - -If both are missing, it returns an error. +`SendSMS` requires `Token`. If it is missing, it returns an error. ### Example ```go -result, err := wenova.SendOtp(ctx, wenova.SendOtpRequest{ +result, err := wenova.SendSMS(ctx, wenova.SendSMSRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", Token: "your-token", - UsePackage: true, }) ``` -### Helpers - -The package also includes: - -- `ScriptID(string) int64` for parsing a positive script ID from a string - -## Address - -The address helpers fetch Wenova Link location data for provinces, districts, and villages. - -### Options - -`Options` supports: - -- `PluginKey` for API access -- `KW` for keyword filtering -- `Lang` for response language -- `BaseURL` for overriding the API host - -### Available Functions - -- `GetProvinces` -- `GetProvinceById` -- `GetDistrictsByProvince` -- `GetDistrictById` -- `GetVillagesByDistrict` -- `GetVillageById` - -### Example - -```go -districts, err := wenova.GetDistrictsByProvince(ctx, 1, wenova.Options{ - PluginKey: "your-plugin-key", - KW: "chan", - Lang: "en", -}) -``` - -### Validation - -The address helpers enforce: - -- `PluginKey` must not be empty -- numeric IDs must be positive - ## Error Behavior -Both request areas: - - return decoded JSON as `any` on success - return `nil, nil` for empty successful response bodies - parse API error payloads and include the HTTP status code in returned errors @@ -169,15 +89,11 @@ Run all tests with: go test ./... ``` -The live Wenova address test reads `WENOVA_PLUGIN_KEY`. If it is not set, the test is skipped. - -The live Wenova service API test reads: +The live Wenova SMS test reads: - `WENOVA_TOKEN` -- `WENOVA_SCRIPT_ID` - `WENOVA_DEMO_PHONE_NUMBER` - `WENOVA_DEMO_HEADER` (optional) - `WENOVA_DEMO_MESSAGE` (optional) -- `WENOVA_DEMO_USE_PACKAGE` (optional) -The service test requires `WENOVA_DEMO_PHONE_NUMBER` and at least one of `WENOVA_TOKEN` or `WENOVA_SCRIPT_ID`. `WENOVA_DEMO_USE_PACKAGE` defaults to `false` and should only be enabled when the credential is linked to a valid package. In GitHub Actions, provide these values as repository secrets with the same names. +The SMS test requires `WENOVA_TOKEN` and `WENOVA_DEMO_PHONE_NUMBER`. In GitHub Actions, provide these values as repository secrets with the same names. diff --git a/wenova/address.go b/wenova/address.go deleted file mode 100644 index b10d32b..0000000 --- a/wenova/address.go +++ /dev/null @@ -1,181 +0,0 @@ -package wenova - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "strings" - "time" -) - -// Options carries pluginKey and optional query flags. -type Options struct { - PluginKey string - KW string - BaseURL string - Lang string -} - -func assertPluginKey(key string) error { - if strings.TrimSpace(key) == "" { - return errors.New("pluginKey is required") - } - return nil -} - -func npmQuery(pluginKey string, opts Options) map[string]string { - q := map[string]string{"key": pluginKey} - if strings.TrimSpace(opts.KW) != "" { - q["kw"] = opts.KW - } - if strings.TrimSpace(opts.Lang) != "" { - q["lang"] = opts.Lang - } - return q -} - -func getJSON(ctx context.Context, url string, query map[string]string) (any, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - q := req.URL.Query() - for k, v := range query { - q.Set(k, v) - } - req.URL.RawQuery = q.Encode() - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, ErrorFromResponseBody(b, resp.StatusCode, "Request failed") - } - var out any - if len(b) == 0 { - return nil, nil - } - if err := json.Unmarshal(b, &out); err != nil { - return nil, fmt.Errorf("decode response: %w", err) - } - return out, nil -} - -func assertPositiveID(name string, id int64) error { - if id <= 0 { - return fmt.Errorf("%s must be a positive number", name) - } - return nil -} - -// GetProvinces GET /npm-provinces?key=&kw=&lang= -func GetProvinces(ctx context.Context, opts Options) (any, error) { - return Wenova{}.GetProvinces(ctx, opts) -} - -func (Wenova) GetProvinces(ctx context.Context, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-provinces" - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} - -// GetProvinceById GET /npm-provinces/:id?key=&lang= -func GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenova{}.GetProvinceById(ctx, id, opts) -} - -func (Wenova) GetProvinceById(ctx context.Context, id int64, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - if err := assertPositiveID("id", id); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-provinces/" + strconv.FormatInt(id, 10) - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} - -// GetDistrictsByProvince GET /npm-districts/by-province/:provinceId?key=&kw=&lang= -func GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { - return Wenova{}.GetDistrictsByProvince(ctx, provinceID, opts) -} - -func (Wenova) GetDistrictsByProvince(ctx context.Context, provinceID int64, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - if err := assertPositiveID("provinceId", provinceID); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-districts/by-province/" + strconv.FormatInt(provinceID, 10) - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} - -// GetDistrictById GET /npm-districts/:id?key=&lang= -func GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenova{}.GetDistrictById(ctx, id, opts) -} - -func (Wenova) GetDistrictById(ctx context.Context, id int64, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - if err := assertPositiveID("id", id); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-districts/" + strconv.FormatInt(id, 10) - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} - -// GetVillagesByDistrict GET /npm-vilages/by-district/:districtId?key=&kw=&lang= -func GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { - return Wenova{}.GetVillagesByDistrict(ctx, districtID, opts) -} - -func (Wenova) GetVillagesByDistrict(ctx context.Context, districtID int64, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - if err := assertPositiveID("districtId", districtID); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-vilages/by-district/" + strconv.FormatInt(districtID, 10) - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} - -// GetVillageById GET /npm-vilages/:id?key=&lang= -func GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { - return Wenova{}.GetVillageById(ctx, id, opts) -} - -func (Wenova) GetVillageById(ctx context.Context, id int64, opts Options) (any, error) { - if err := assertPluginKey(opts.PluginKey); err != nil { - return nil, err - } - if err := assertPositiveID("id", id); err != nil { - return nil, err - } - base := ResolveBaseURL(opts.BaseURL) - url := base + "/npm-vilages/" + strconv.FormatInt(id, 10) - return getJSON(ctx, url, npmQuery(opts.PluginKey, opts)) -} diff --git a/wenova/apiclient.go b/wenova/apiclient.go deleted file mode 100644 index 11358bc..0000000 --- a/wenova/apiclient.go +++ /dev/null @@ -1,60 +0,0 @@ -package wenova - -import ( - "encoding/json" - "fmt" - "os" - "strings" -) - -const DefaultBaseURL = "https://apimicroservices.wenova.fun" - -// ResolveBaseURL returns override if set, else WENOVA_API_URL, else DefaultBaseURL. -func ResolveBaseURL(override string) string { - if strings.TrimSpace(override) != "" { - return strings.TrimSuffix(strings.TrimSpace(override), "/") - } - if v := strings.TrimSpace(os.Getenv("WENOVA_API_URL")); v != "" { - return strings.TrimSuffix(v, "/") - } - return strings.TrimSuffix(DefaultBaseURL, "/") -} - -// ResolveBaseURL returns override if set, else WENOVA_API_URL, else DefaultBaseURL. -func (Wenova) ResolveBaseURL(override string) string { - return ResolveBaseURL(override) -} - -// ErrorFromResponseBody parses JSON error bodies like the Node SDKs. -func ErrorFromResponseBody(b []byte, status int, fallback string) error { - var wrap struct { - Message any `json:"message"` - Error any `json:"error"` - } - msg := fallback - if json.Unmarshal(b, &wrap) == nil { - if s, ok := stringifyErrPart(wrap.Message); ok && s != "" { - msg = s - } else if s, ok := stringifyErrPart(wrap.Error); ok && s != "" { - msg = s - } - } - return fmt.Errorf("%s (HTTP %d)", msg, status) -} - -func stringifyErrPart(v any) (string, bool) { - switch x := v.(type) { - case string: - return x, true - case map[string]any: - if m, ok := x["message"].(string); ok { - return m, true - } - if raw, err := json.Marshal(x); err == nil { - return string(raw), true - } - case float64, bool, json.Number: - return fmt.Sprint(x), true - } - return "", false -} diff --git a/wenova/sms.go b/wenova/sms.go new file mode 100644 index 0000000..4c29bc0 --- /dev/null +++ b/wenova/sms.go @@ -0,0 +1,115 @@ +package wenova + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// SendSMSRequest matches the Wenova SMS payload. +type SendSMSRequest struct { + Header string + PhoneNumber string + Message string + Token string +} + +// SendSMS POSTs to /sms/package and returns the decoded JSON body. +func SendSMS(ctx context.Context, req SendSMSRequest) (any, error) { + return Wenova{}.SendSMS(ctx, req) +} + +func (w Wenova) SendSMS(ctx context.Context, req SendSMSRequest) (any, error) { + if strings.TrimSpace(req.Token) == "" { + return nil, errors.New("token is required") + } + + base := strings.TrimSuffix(strings.TrimSpace(w.BaseUrl), "/") + if base == "" { + base = DefaultBaseURL + } + url := base + "/sms/package" + + body := map[string]any{ + "header": req.Header, + "phoneNumber": req.PhoneNumber, + "message": req.Message, + "token": strings.TrimSpace(req.Token), + } + + raw, err := json.Marshal(body) + if err != nil { + return nil, err + } + + hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return nil, err + } + hreq.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(hreq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, ErrorFromResponseBody(b, resp.StatusCode, "Failed to send SMS") + } + + var out any + if len(b) == 0 { + return nil, nil + } + if err := json.Unmarshal(b, &out); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + return out, nil +} + +// ErrorFromResponseBody parses JSON error bodies like the Node SDKs. +func ErrorFromResponseBody(b []byte, status int, fallback string) error { + var wrap struct { + Message any `json:"message"` + Error any `json:"error"` + } + msg := fallback + if json.Unmarshal(b, &wrap) == nil { + if s, ok := stringifyErrPart(wrap.Message); ok && s != "" { + msg = s + } else if s, ok := stringifyErrPart(wrap.Error); ok && s != "" { + msg = s + } + } + return fmt.Errorf("%s (HTTP %d)", msg, status) +} + +func stringifyErrPart(v any) (string, bool) { + switch x := v.(type) { + case string: + return x, true + case map[string]any: + if m, ok := x["message"].(string); ok { + return m, true + } + if raw, err := json.Marshal(x); err == nil { + return string(raw), true + } + case float64, bool, json.Number: + return fmt.Sprint(x), true + } + return "", false +} diff --git a/wenova/smsotp.go b/wenova/smsotp.go deleted file mode 100644 index 11f5974..0000000 --- a/wenova/smsotp.go +++ /dev/null @@ -1,108 +0,0 @@ -package wenova - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "strings" - "time" -) - -// SendOtpRequest matches the Node SDK payload. -type SendOtpRequest struct { - Header string - PhoneNumber string - Message string - Token string - ScriptID int64 - UsePackage bool - BaseURL string -} - -// SendOtp POSTs to /sms/package and returns the decoded JSON body. -func SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { - return Wenova{}.SendOtp(ctx, req) -} - -func (Wenova) SendOtp(ctx context.Context, req SendOtpRequest) (any, error) { - hasToken := strings.TrimSpace(req.Token) != "" - hasScript := req.ScriptID > 0 - if !hasToken && !hasScript { - return nil, errors.New("either token or scriptId is required") - } - - base := ResolveBaseURL(req.BaseURL) - url := base + "/sms/package" - - body := map[string]any{ - "header": req.Header, - "phoneNumber": req.PhoneNumber, - "message": req.Message, - "usePackage": req.UsePackage, - } - if hasToken { - body["token"] = strings.TrimSpace(req.Token) - } - if hasScript { - body["scriptId"] = req.ScriptID - } - - raw, err := json.Marshal(body) - if err != nil { - return nil, err - } - - hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return nil, err - } - hreq.Header.Set("Content-Type", "application/json") - - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(hreq) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, ErrorFromResponseBody(b, resp.StatusCode, "Failed to send OTP") - } - - var out any - if len(b) == 0 { - return nil, nil - } - if err := json.Unmarshal(b, &out); err != nil { - return nil, fmt.Errorf("decode response: %w", err) - } - return out, nil -} - -// ScriptID parses a positive script id from string. -func ScriptID(s string) int64 { - s = strings.TrimSpace(s) - if s == "" { - return 0 - } - n, err := strconv.ParseInt(s, 10, 64) - if err != nil || n <= 0 { - return 0 - } - return n -} - -// ScriptID parses a positive script id from string. -func (Wenova) ScriptID(s string) int64 { - return ScriptID(s) -} diff --git a/wenova/wenova.go b/wenova/wenova.go index 8d4e669..2c7f77c 100644 --- a/wenova/wenova.go +++ b/wenova/wenova.go @@ -1,3 +1,7 @@ package wenova -type Wenova struct{} +const DefaultBaseURL = "https://apimicroservices.wenova.fun" + +type Wenova struct { + BaseUrl string +} diff --git a/wenova/wenova_test.go b/wenova/wenova_test.go index 60af01b..731b947 100644 --- a/wenova/wenova_test.go +++ b/wenova/wenova_test.go @@ -13,43 +13,10 @@ func requiredTrimmedEnv(key string) string { return strings.TrimSpace(os.Getenv(key)) } -func optionalBoolEnv(key string) bool { - v := strings.ToLower(requiredTrimmedEnv(key)) - return v == "1" || v == "true" || v == "yes" -} - -func TestResolveBaseURL(t *testing.T) { - t.Setenv("WENOVA_API_URL", "") - if got := ResolveBaseURL(""); got != DefaultBaseURL { - t.Fatalf("expected default base url %q, got %q", DefaultBaseURL, got) - } - - t.Setenv("WENOVA_API_URL", "https://example.com/") - if got := ResolveBaseURL(""); got != "https://example.com" { - t.Fatalf("expected env base url to be trimmed, got %q", got) - } - - if got := ResolveBaseURL(" https://override.test/ "); got != "https://override.test" { - t.Fatalf("expected override base url to win, got %q", got) - } -} - -func TestScriptID(t *testing.T) { - tests := []struct { - input string - want int64 - }{ - {input: "", want: 0}, - {input: "abc", want: 0}, - {input: "-5", want: 0}, - {input: "42", want: 42}, - {input: " 8 ", want: 8}, - } - - for _, tt := range tests { - if got := ScriptID(tt.input); got != tt.want { - t.Fatalf("ScriptID(%q) = %d, want %d", tt.input, got, tt.want) - } +func TestSendSMSUsesWenovaBaseURL(t *testing.T) { + client := Wenova{BaseUrl: " https://example.com/ "} + if got := strings.TrimSuffix(strings.TrimSpace(client.BaseUrl), "/"); got != "https://example.com" { + t.Fatalf("expected trimmed base url, got %q", got) } } @@ -65,37 +32,15 @@ func TestErrorFromResponseBody(t *testing.T) { } } -func TestGetProvincesLive(t *testing.T) { - pluginKey := requiredTrimmedEnv("WENOVA_PLUGIN_KEY") - if pluginKey == "" { - t.Skip("skipping live Wenova address test: WENOVA_PLUGIN_KEY is not set") - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - result, err := GetProvinces(ctx, Options{ - PluginKey: pluginKey, - Lang: "en", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if result == nil { - t.Fatal("expected province response, got nil") - } -} - -func TestSendOtpLive(t *testing.T) { +func TestSendSMSLive(t *testing.T) { phoneNumber := requiredTrimmedEnv("WENOVA_DEMO_PHONE_NUMBER") if phoneNumber == "" { - t.Skip("skipping live Wenova service test: WENOVA_DEMO_PHONE_NUMBER is not set") + t.Skip("skipping live Wenova SMS test: WENOVA_DEMO_PHONE_NUMBER is not set") } token := requiredTrimmedEnv("WENOVA_TOKEN") - scriptID := ScriptID(requiredTrimmedEnv("WENOVA_SCRIPT_ID")) - if token == "" && scriptID == 0 { - t.Skip("skipping live Wenova service test: WENOVA_TOKEN or WENOVA_SCRIPT_ID is required") + if token == "" { + t.Skip("skipping live Wenova SMS test: WENOVA_TOKEN is not set") } header := requiredTrimmedEnv("WENOVA_DEMO_HEADER") @@ -111,18 +56,16 @@ func TestSendOtpLive(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - result, err := SendOtp(ctx, SendOtpRequest{ + result, err := SendSMS(ctx, SendSMSRequest{ Header: header, PhoneNumber: phoneNumber, Message: message, Token: token, - ScriptID: scriptID, - UsePackage: optionalBoolEnv("WENOVA_DEMO_USE_PACKAGE"), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if result == nil { - t.Fatal("expected OTP response, got nil") + t.Fatal("expected SMS response, got nil") } } From 71ba544b7de80c488f634b5c8146d620b528114a Mon Sep 17 00:00:00 2001 From: Barluscuda Date: Tue, 19 May 2026 16:53:42 +0700 Subject: [PATCH 7/7] update Wenova service to create with token --- wenova/DOCS.md | 14 +++++++++----- wenova/sms.go | 11 +++-------- wenova/wenova.go | 1 + wenova/wenova_test.go | 9 ++++++--- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/wenova/DOCS.md b/wenova/DOCS.md index e57d126..f040dcc 100644 --- a/wenova/DOCS.md +++ b/wenova/DOCS.md @@ -20,13 +20,13 @@ func main() { ctx := context.Background() client := wenova.Wenova{ BaseUrl: "https://apimicroservices.wenova.fun", + Token: "your-token", } smsResult, err := client.SendSMS(ctx, wenova.SendSMSRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", - Token: "your-token", }) if err != nil { panic(err) @@ -54,7 +54,7 @@ Like `envtools`, `wenova` uses a flat package structure: - `sms.go` contains SMS helpers - `wenova_test.go` covers shared behavior -You can call helpers either from package-level functions or from a `Wenova` value. +Call helpers from a configured `Wenova` value. ## SMS @@ -62,16 +62,20 @@ You can call helpers either from package-level functions or from a `Wenova` valu ### Request Rules -`SendSMS` requires `Token`. If it is missing, it returns an error. +`SendSMS` requires `Wenova.Token`. If it is missing, it returns an error. ### Example ```go -result, err := wenova.SendSMS(ctx, wenova.SendSMSRequest{ +client := wenova.Wenova{ + BaseUrl: "https://apimicroservices.wenova.fun", + Token: "your-token", +} + +result, err := client.SendSMS(ctx, wenova.SendSMSRequest{ Header: "WNV-OTP", PhoneNumber: "2012345678", Message: "Code: 123456", - Token: "your-token", }) ``` diff --git a/wenova/sms.go b/wenova/sms.go index 4c29bc0..109032f 100644 --- a/wenova/sms.go +++ b/wenova/sms.go @@ -17,16 +17,11 @@ type SendSMSRequest struct { Header string PhoneNumber string Message string - Token string -} - -// SendSMS POSTs to /sms/package and returns the decoded JSON body. -func SendSMS(ctx context.Context, req SendSMSRequest) (any, error) { - return Wenova{}.SendSMS(ctx, req) } func (w Wenova) SendSMS(ctx context.Context, req SendSMSRequest) (any, error) { - if strings.TrimSpace(req.Token) == "" { + token := strings.TrimSpace(w.Token) + if token == "" { return nil, errors.New("token is required") } @@ -40,7 +35,7 @@ func (w Wenova) SendSMS(ctx context.Context, req SendSMSRequest) (any, error) { "header": req.Header, "phoneNumber": req.PhoneNumber, "message": req.Message, - "token": strings.TrimSpace(req.Token), + "token": token, } raw, err := json.Marshal(body) diff --git a/wenova/wenova.go b/wenova/wenova.go index 2c7f77c..4d38391 100644 --- a/wenova/wenova.go +++ b/wenova/wenova.go @@ -4,4 +4,5 @@ const DefaultBaseURL = "https://apimicroservices.wenova.fun" type Wenova struct { BaseUrl string + Token string } diff --git a/wenova/wenova_test.go b/wenova/wenova_test.go index 731b947..47f60d5 100644 --- a/wenova/wenova_test.go +++ b/wenova/wenova_test.go @@ -14,10 +14,13 @@ func requiredTrimmedEnv(key string) string { } func TestSendSMSUsesWenovaBaseURL(t *testing.T) { - client := Wenova{BaseUrl: " https://example.com/ "} + client := Wenova{BaseUrl: " https://example.com/ ", Token: "demo-token"} if got := strings.TrimSuffix(strings.TrimSpace(client.BaseUrl), "/"); got != "https://example.com" { t.Fatalf("expected trimmed base url, got %q", got) } + if client.Token != "demo-token" { + t.Fatalf("expected token on client, got %q", client.Token) + } } func TestErrorFromResponseBody(t *testing.T) { @@ -56,11 +59,11 @@ func TestSendSMSLive(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - result, err := SendSMS(ctx, SendSMSRequest{ + client := Wenova{Token: token} + result, err := client.SendSMS(ctx, SendSMSRequest{ Header: header, PhoneNumber: phoneNumber, Message: message, - Token: token, }) if err != nil { t.Fatalf("unexpected error: %v", err)