Skip to content

Commit 7678e8d

Browse files
committed
Add tests for the json_validation example.
Signed-off-by: Hiram Chirino <hiram@hiramchirino.com>
1 parent 8ad9d08 commit 7678e8d

File tree

6 files changed

+305
-0
lines changed

6 files changed

+305
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Attribution
2+
3+
This example originally came from:
4+
https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation
5+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/json_validation
2+
3+
go 1.24
4+
5+
require (
6+
github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924
7+
github.com/tidwall/gjson v1.18.0
8+
)
9+
10+
require (
11+
github.com/tidwall/match v1.1.1 // indirect
12+
github.com/tidwall/pretty v1.2.0 // indirect
13+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 h1:wTcK6gcyTKJMeDka69AMjZYvisdI8CBXzTEfZ+2pOxI=
6+
github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924/go.mod h1:9mBRvh8I6Td6sg3CwEY+zGFE4DKaIoieCaca1kQnDBE=
7+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
10+
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
11+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
12+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
13+
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
14+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
15+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
16+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm"
7+
"github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types"
8+
"github.com/tidwall/gjson"
9+
)
10+
11+
func main() {}
12+
func init() {
13+
// SetVMContext is the entrypoint for setting up this entire Wasm VM.
14+
// Please make sure that this entrypoint be called during "main()" function, otherwise
15+
// this VM would fail.
16+
proxywasm.SetVMContext(&vmContext{})
17+
}
18+
19+
// vmContext implements types.VMContext.
20+
type vmContext struct {
21+
// Embed the default VM context here,
22+
// so that we don't need to reimplement all the methods.
23+
types.DefaultVMContext
24+
}
25+
26+
// NewPluginContext implements types.VMContext.
27+
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
28+
return &pluginContext{}
29+
}
30+
31+
// pluginContext implements types.PluginContext.
32+
type pluginContext struct {
33+
// Embed the default plugin context here,
34+
// so that we don't need to reimplement all the methods.
35+
types.DefaultPluginContext
36+
configuration pluginConfiguration
37+
}
38+
39+
// pluginConfiguration is a type to represent an example configuration for this wasm plugin.
40+
type pluginConfiguration struct {
41+
// Example configuration field.
42+
// The plugin will validate if those fields exist in the json payload.
43+
requiredKeys []string
44+
}
45+
46+
// OnPluginStart implements types.PluginContext.
47+
func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
48+
data, err := proxywasm.GetPluginConfiguration()
49+
if err != nil && err != types.ErrorStatusNotFound {
50+
proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
51+
return types.OnPluginStartStatusFailed
52+
}
53+
config, err := parsePluginConfiguration(data)
54+
if err != nil {
55+
proxywasm.LogCriticalf("error parsing plugin configuration: %v", err)
56+
return types.OnPluginStartStatusFailed
57+
}
58+
ctx.configuration = config
59+
return types.OnPluginStartStatusOK
60+
}
61+
62+
// parsePluginConfiguration parses the json plugin configuration data and returns pluginConfiguration.
63+
// Note that this parses the json data by gjson, since TinyGo doesn't support encoding/json.
64+
// You can also try https://github.com/mailru/easyjson, which supports decoding to a struct.
65+
func parsePluginConfiguration(data []byte) (pluginConfiguration, error) {
66+
if len(data) == 0 {
67+
return pluginConfiguration{}, nil
68+
}
69+
70+
config := &pluginConfiguration{}
71+
if !gjson.ValidBytes(data) {
72+
return pluginConfiguration{}, fmt.Errorf("the plugin configuration is not a valid json: %q", string(data))
73+
}
74+
75+
jsonData := gjson.ParseBytes(data)
76+
requiredKeys := jsonData.Get("requiredKeys").Array()
77+
for _, requiredKey := range requiredKeys {
78+
config.requiredKeys = append(config.requiredKeys, requiredKey.Str)
79+
}
80+
81+
return *config, nil
82+
}
83+
84+
// NewHttpContext implements types.PluginContext.
85+
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
86+
return &payloadValidationContext{requiredKeys: ctx.configuration.requiredKeys}
87+
}
88+
89+
// payloadValidationContext implements types.HttpContext.
90+
type payloadValidationContext struct {
91+
// Embed the default root http context here,
92+
// so that we don't need to reimplement all the methods.
93+
types.DefaultHttpContext
94+
totalRequestBodySize int
95+
requiredKeys []string
96+
}
97+
98+
// OnHttpRequestHeaders implements types.HttpContext.
99+
func (*payloadValidationContext) OnHttpRequestHeaders(numHeaders int, _ bool) types.Action {
100+
contentType, err := proxywasm.GetHttpRequestHeader("content-type")
101+
if err != nil || contentType != "application/json" {
102+
// If the header doesn't have the expected content value, send the 403 response,
103+
if err := proxywasm.SendHttpResponse(403, nil, []byte("content-type must be provided"), -1); err != nil {
104+
panic(err)
105+
}
106+
// and terminates the further processing of this traffic by ActionPause.
107+
return types.ActionPause
108+
}
109+
110+
// ActionContinue lets the host continue the processing the body.
111+
return types.ActionContinue
112+
}
113+
114+
// OnHttpRequestBody implements types.HttpContext.
115+
func (ctx *payloadValidationContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
116+
ctx.totalRequestBodySize += bodySize
117+
if !endOfStream {
118+
// OnHttpRequestBody may be called each time a part of the body is received.
119+
// Wait until we see the entire body to replace.
120+
return types.ActionPause
121+
}
122+
123+
body, err := proxywasm.GetHttpRequestBody(0, ctx.totalRequestBodySize)
124+
if err != nil {
125+
proxywasm.LogErrorf("failed to get request body: %v", err)
126+
return types.ActionContinue
127+
}
128+
if !ctx.validatePayload(body) {
129+
// If the validation fails, send the 403 response,
130+
if err := proxywasm.SendHttpResponse(403, nil, []byte("invalid payload"), -1); err != nil {
131+
proxywasm.LogErrorf("failed to send the 403 response: %v", err)
132+
}
133+
// and terminates this traffic.
134+
return types.ActionPause
135+
}
136+
137+
return types.ActionContinue
138+
}
139+
140+
// validatePayload validates the given json payload.
141+
// Note that this function parses the json data by gjson, since TinyGo doesn't support encoding/json.
142+
func (ctx *payloadValidationContext) validatePayload(body []byte) bool {
143+
if !gjson.ValidBytes(body) {
144+
proxywasm.LogErrorf("body is not a valid json: %q", string(body))
145+
return false
146+
}
147+
jsonData := gjson.ParseBytes(body)
148+
149+
// Do any validation on the json. Check if required keys exist here as an example.
150+
// The required keys are configurable via the plugin configuration.
151+
for _, requiredKey := range ctx.requiredKeys {
152+
if !jsonData.Get(requiredKey).Exists() {
153+
proxywasm.LogErrorf("required key (%v) is missing: %v", requiredKey, jsonData)
154+
return false
155+
}
156+
}
157+
158+
return true
159+
}
3.27 MB
Binary file not shown.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.roastedroot.proxywasm;
2+
3+
import static io.roastedroot.proxywasm.v1.Helpers.bytes;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import com.dylibso.chicory.wasm.Parser;
7+
import io.roastedroot.proxywasm.v1.Action;
8+
import io.roastedroot.proxywasm.v1.HttpContext;
9+
import io.roastedroot.proxywasm.v1.ProxyWasm;
10+
import io.roastedroot.proxywasm.v1.StartException;
11+
import java.nio.file.Path;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.AfterEach;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Nested;
16+
import org.junit.jupiter.api.Test;
17+
18+
/**
19+
* Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation/main_test.go
20+
*/
21+
public class JsonValidationTest {
22+
private final MockHandler handler = new MockHandler();
23+
24+
@Nested
25+
class OnHttpRequestHeaders {
26+
private ProxyWasm host;
27+
private HttpContext context;
28+
29+
@BeforeEach
30+
void setUp() throws StartException {
31+
var module = Parser.parse(Path.of("./src/test/go-examples/json_validation/main.wasm"));
32+
this.host = ProxyWasm.builder().build(module);
33+
this.context = host.createHttpContext(handler);
34+
}
35+
36+
@AfterEach
37+
void tearDown() {
38+
context.close();
39+
host.close();
40+
}
41+
42+
@Test
43+
void testFailsDueToUnsupportedContentType() {
44+
var contentType = "application/json";
45+
var expectedAction = Action.CONTINUE;
46+
47+
handler.setHttpRequestHeaders(Map.of("content-type", contentType));
48+
var action = context.callOnRequestHeaders(false);
49+
assertEquals(expectedAction, action);
50+
}
51+
52+
@Test
53+
void successForJson() {
54+
var contentType = "application/json";
55+
var expectedAction = Action.CONTINUE;
56+
57+
handler.setHttpRequestHeaders(Map.of("content-type", contentType));
58+
var action = context.callOnRequestHeaders(false);
59+
assertEquals(expectedAction, action);
60+
}
61+
}
62+
63+
@Nested
64+
class OnHttpRequestBody {
65+
private ProxyWasm host;
66+
private HttpContext context;
67+
68+
@BeforeEach
69+
void setUp() throws StartException {
70+
var config = "{\"requiredKeys\": [\"my_key\"]}";
71+
var module = Parser.parse(Path.of("./src/test/go-examples/json_validation/main.wasm"));
72+
this.host = ProxyWasm.builder().withPluginConfig(config).build(module);
73+
this.context = host.createHttpContext(handler);
74+
}
75+
76+
@AfterEach
77+
void tearDown() {
78+
context.close();
79+
host.close();
80+
}
81+
82+
@Test
83+
public void pausesDueToInvalidPayload() {
84+
String body = "invalid_payload";
85+
Action expectedAction = Action.PAUSE;
86+
87+
handler.setHttpRequestBody(bytes(body));
88+
var action = context.callOnRequestBody(true);
89+
assertEquals(expectedAction, action);
90+
}
91+
92+
@Test
93+
public void pausesDueToUnknownKeys() {
94+
String body = "{\"unknown_key\":\"unknown_value\"}";
95+
Action expectedAction = Action.PAUSE;
96+
97+
handler.setHttpRequestBody(bytes(body));
98+
var action = context.callOnRequestBody(true);
99+
assertEquals(expectedAction, action);
100+
}
101+
102+
@Test
103+
public void success() {
104+
String body = "{\"my_key\":\"my_value\"}";
105+
Action expectedAction = Action.CONTINUE;
106+
107+
handler.setHttpRequestBody(bytes(body));
108+
var action = context.callOnRequestBody(true);
109+
assertEquals(expectedAction, action);
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)