Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:

- name: Run unit tests
env:
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 }}
MINECRAFT_MCSERVER_DEMO_IP: ${{ secrets.MINECRAFT_MCSERVER_DEMO_IP }}
MINECRAFT_MCSERVER_DEMO_PORT: ${{ secrets.MINECRAFT_MCSERVER_DEMO_PORT }}
run: go test ./...
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,13 @@ go test ./...

GitHub Actions is configured to run the test suite on `push` and `pull_request`.

The live Bedrock status test reads these environment variables:
Some live integration tests read repository secrets through environment variables:

- `MINECRAFT_MCSERVER_DEMO_IP`
- `MINECRAFT_MCSERVER_DEMO_PORT`
- `WENOVA_TOKEN`
- `WENOVA_DEMO_PHONE_NUMBER`
- `WENOVA_DEMO_HEADER`
- `WENOVA_DEMO_MESSAGE`

In GitHub Actions, set them as repository secrets with the same names. When they are not set, the live Bedrock test is skipped.
In GitHub Actions, set these as repository secrets with the same names. When they are not set, the corresponding live tests are skipped.
103 changes: 103 additions & 0 deletions wenova/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# wenova

`wenova` 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/wenova"
)

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",
})
if err != nil {
panic(err)
}

fmt.Println(smsResult)
}
```

## Packages

The package currently supports:

- `SendSMS`

## Configuration

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
- `sms.go` contains SMS helpers
- `wenova_test.go` covers shared behavior

Call helpers from a configured `Wenova` value.

## SMS

`SendSMS` sends SMS requests to `POST /sms/package`.

### Request Rules

`SendSMS` requires `Wenova.Token`. If it is missing, it returns an error.

### Example

```go
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",
})
```

## Error Behavior

- 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 ./...
```

The live Wenova SMS test reads:

- `WENOVA_TOKEN`
- `WENOVA_DEMO_PHONE_NUMBER`
- `WENOVA_DEMO_HEADER` (optional)
- `WENOVA_DEMO_MESSAGE` (optional)

The SMS test requires `WENOVA_TOKEN` and `WENOVA_DEMO_PHONE_NUMBER`. In GitHub Actions, provide these values as repository secrets with the same names.
110 changes: 110 additions & 0 deletions wenova/sms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
}

func (w Wenova) SendSMS(ctx context.Context, req SendSMSRequest) (any, error) {
token := strings.TrimSpace(w.Token)
if 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": 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
}
8 changes: 8 additions & 0 deletions wenova/wenova.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wenova

const DefaultBaseURL = "https://apimicroservices.wenova.fun"

type Wenova struct {
BaseUrl string
Token string
}
74 changes: 74 additions & 0 deletions wenova/wenova_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package wenova

import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
)

func requiredTrimmedEnv(key string) string {
return strings.TrimSpace(os.Getenv(key))
}

func TestSendSMSUsesWenovaBaseURL(t *testing.T) {
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) {
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)
}
}

func TestSendSMSLive(t *testing.T) {
phoneNumber := requiredTrimmedEnv("WENOVA_DEMO_PHONE_NUMBER")
if phoneNumber == "" {
t.Skip("skipping live Wenova SMS test: WENOVA_DEMO_PHONE_NUMBER is not set")
}

token := requiredTrimmedEnv("WENOVA_TOKEN")
if token == "" {
t.Skip("skipping live Wenova SMS test: WENOVA_TOKEN is not set")
}

header := requiredTrimmedEnv("WENOVA_DEMO_HEADER")
if header == "" {
header = "WNV-OTP"
}

message := requiredTrimmedEnv("WENOVA_DEMO_MESSAGE")
if message == "" {
message = fmt.Sprintf("Code: %d", time.Now().Unix()%1000000)
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

client := Wenova{Token: token}
result, err := client.SendSMS(ctx, SendSMSRequest{
Header: header,
PhoneNumber: phoneNumber,
Message: message,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected SMS response, got nil")
}
}
Loading