| # |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| use t::APISIX 'no_plan'; |
| |
| no_long_string(); |
| no_shuffle(); |
| no_root_location(); |
| |
| add_block_preprocessor(sub { |
| my ($block) = @_; |
| |
| if (!$block->request) { |
| $block->set_value("request", "GET /t"); |
| } |
| }); |
| |
| run_tests; |
| |
| __DATA__ |
| |
| === TEST 1: set rule |
| --- config |
| location /t { |
| content_by_lua_block { |
| local http = require "resty.http" |
| local t = require("lib.test_admin").test |
| local code, body = t('/apisix/admin/protos/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "content" : "syntax = \"proto3\"; |
| package helloworld; |
| service Greeter { |
| rpc SayMultipleHello(MultipleHelloRequest) returns (MultipleHelloReply) {} |
| } |
| |
| enum Gender { |
| GENDER_UNKNOWN = 0; |
| GENDER_MALE = 1; |
| GENDER_FEMALE = 2; |
| } |
| |
| message Person { |
| string name = 1; |
| int32 age = 2; |
| } |
| |
| message MultipleHelloRequest { |
| string name = 1; |
| repeated string items = 2; |
| repeated Gender genders = 3; |
| repeated Person persons = 4; |
| } |
| |
| message MultipleHelloReply{ |
| string message = 1; |
| }" |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.say(body) |
| return |
| end |
| |
| local code, body = t('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "methods": ["POST"], |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "helloworld.Greeter", |
| "method": "SayMultipleHello" |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.say(body) |
| return |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 2: hit route |
| --- request |
| POST /grpctest |
| {"name":"world","persons":[{"name":"Joe","age":1},{"name":"Jake","age":2}]} |
| --- more_headers |
| Content-Type: application/json |
| --- response_body chomp |
| {"message":"Hello world, name: Joe, age: 1, name: Jake, age: 2"} |
| |
| |
| |
| === TEST 3: set proto (id: 1, get error response from rpc) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| |
| local code, body = t('/apisix/admin/protos/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "content" : "syntax = \"proto3\"; |
| package helloworld; |
| service Greeter { |
| rpc GetErrResp (HelloRequest) returns (HelloReply) {} |
| } |
| message HelloRequest { |
| string name = 1; |
| repeated string items = 2; |
| } |
| message HelloReply { |
| string message = 1; |
| repeated string items = 2; |
| } |
| message ErrorDetail { |
| int64 code = 1; |
| string message = 2; |
| string type = 3; |
| }" |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| return |
| end |
| |
| local code, body = t('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "methods": ["GET", "POST"], |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "helloworld.Greeter", |
| "method": "GetErrResp" |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 4: hit route (error response in header) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local json = require("toolkit.json") |
| local t = require("lib.test_admin").test |
| |
| local code, body, headers = t('/grpctest?name=world', |
| ngx.HTTP_GET |
| ) |
| |
| ngx.status = code |
| |
| ngx.header['grpc-status'] = headers['grpc-status'] |
| ngx.header['grpc-message'] = headers['grpc-message'] |
| ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin'] |
| |
| body = json.encode(body) |
| ngx.say(body) |
| } |
| } |
| --- response_headers |
| grpc-status: 14 |
| grpc-message: Out of service |
| grpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U |
| --- response_body_unlike eval |
| qr/error/ |
| --- error_code: 503 |
| |
| |
| |
| === TEST 5: set routes (id: 1, show error response in body) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| |
| local code, body = t('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "methods": ["GET", "POST"], |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "helloworld.Greeter", |
| "method": "GetErrResp", |
| "show_status_in_body": true |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 6: hit route (show error status in body) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local json = require("toolkit.json") |
| local t = require("lib.test_admin").test |
| |
| local code, body, headers = t('/grpctest?name=world', |
| ngx.HTTP_GET |
| ) |
| |
| ngx.status = code |
| |
| ngx.header['grpc-status'] = headers['grpc-status'] |
| ngx.header['grpc-message'] = headers['grpc-message'] |
| ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin'] |
| |
| body = json.decode(body) |
| body = json.encode(body) |
| ngx.say(body) |
| } |
| } |
| --- response_headers |
| grpc-status: 14 |
| grpc-message: Out of service |
| grpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U |
| --- response_body |
| {"error":{"code":14,"details":[{"type_url":"type.googleapis.com/helloworld.ErrorDetail","value":"\b\u0001\u0012\u001cThe server is out of service\u001a\u0007service"}],"message":"Out of service"}} |
| --- error_code: 503 |
| |
| |
| |
| === TEST 7: set routes (id: 1, show error details in body) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| |
| local code, body = t('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "methods": ["GET", "POST"], |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "helloworld.Greeter", |
| "method": "GetErrResp", |
| "show_status_in_body": true, |
| "status_detail_type": "helloworld.ErrorDetail" |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 8: hit route (show error details in body) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local json = require("toolkit.json") |
| local t = require("lib.test_admin").test |
| |
| local code, body, headers = t('/grpctest?name=world', |
| ngx.HTTP_GET |
| ) |
| |
| ngx.status = code |
| |
| ngx.header['grpc-status'] = headers['grpc-status'] |
| ngx.header['grpc-message'] = headers['grpc-message'] |
| ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin'] |
| |
| body = json.decode(body) |
| body = json.encode(body) |
| ngx.say(body) |
| } |
| } |
| --- response_headers |
| grpc-status: 14 |
| grpc-message: Out of service |
| grpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U |
| --- response_body |
| {"error":{"code":14,"details":[{"code":1,"message":"The server is out of service","type":"service"}],"message":"Out of service"}} |
| --- error_code: 503 |
| |
| |
| |
| === TEST 9: set routes (id: 1, show error details in body and wrong status_detail_type) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin").test |
| |
| local code, body = t('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "methods": ["GET", "POST"], |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "helloworld.Greeter", |
| "method": "GetErrResp", |
| "show_status_in_body": true, |
| "status_detail_type": "helloworld.error" |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 10: hit route (show error details in body and wrong status_detail_type) |
| --- config |
| location /t { |
| content_by_lua_block { |
| local json = require("toolkit.json") |
| local t = require("lib.test_admin").test |
| |
| local code, body, headers = t('/grpctest?name=world', |
| ngx.HTTP_GET |
| ) |
| |
| ngx.status = code |
| |
| ngx.header['grpc-status'] = headers['grpc-status'] |
| ngx.header['grpc-message'] = headers['grpc-message'] |
| ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin'] |
| |
| ngx.say(body) |
| } |
| } |
| --- response_headers |
| grpc-status: 14 |
| grpc-message: Out of service |
| grpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U |
| --- response_body |
| failed to call pb.decode to decode details in grpc-status-details-bin |
| --- error_log |
| transform response error: failed to call pb.decode to decode details in grpc-status-details-bin, err: |
| --- error_code: 503 |
| |
| |
| |
| === TEST 11: set binary rule for EchoStruct |
| --- config |
| location /t { |
| content_by_lua_block { |
| local t = require("lib.test_admin") |
| local json = require("toolkit.json") |
| |
| local content = t.read_file("t/grpc_server_example/echo.pb") |
| local data = {content = ngx.encode_base64(content)} |
| local code, body = t.test('/apisix/admin/protos/1', |
| ngx.HTTP_PUT, |
| json.encode(data) |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| ngx.say(body) |
| return |
| end |
| |
| local code, body = t.test('/apisix/admin/routes/1', |
| ngx.HTTP_PUT, |
| [[{ |
| "uri": "/grpctest", |
| "plugins": { |
| "grpc-transcode": { |
| "proto_id": "1", |
| "service": "echo.Echo", |
| "method": "EchoStruct" |
| } |
| }, |
| "upstream": { |
| "scheme": "grpc", |
| "type": "roundrobin", |
| "nodes": { |
| "127.0.0.1:10051": 1 |
| } |
| } |
| }]] |
| ) |
| |
| if code >= 300 then |
| ngx.status = code |
| end |
| ngx.say(body) |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 12: hit route to test EchoStruct |
| --- config |
| location /t { |
| content_by_lua_block { |
| local core = require "apisix.core" |
| local http = require "resty.http" |
| local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/grpctest" |
| local body = [[{"data":{"fields":{"foo":{"string_value":"xxx"},"bar":{"number_value":666}}}}]] |
| local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}, keepalive = false} |
| local httpc = http.new() |
| local res, err = httpc:request_uri(uri, opt) |
| if not res then |
| ngx.log(ngx.ERR, err) |
| return ngx.exit(500) |
| end |
| if res.status > 300 then |
| return ngx.exit(res.status) |
| else |
| local req = core.json.decode(body) |
| local rsp = core.json.decode(res.body) |
| for k, v in pairs(req.data.fields) do |
| if rsp.data.fields[k] == nil then |
| ngx.log(ngx.ERR, "rsp missing field=", k, ", rsp: ", res.body) |
| else |
| for k1, v1 in pairs(v) do |
| if v1 ~= rsp.data.fields[k][k1] then |
| ngx.log(ngx.ERR, "rsp mismatch: k=", k1, |
| ", req=", v1, ", rsp=", rsp.data.fields[k][k1]) |
| end |
| end |
| end |
| end |
| end |
| } |
| } |
| |
| |
| |
| === TEST 13: bugfix - filter out illegal INT(string) formats |
| --- config |
| location /t { |
| content_by_lua_block { |
| local pcall = pcall |
| local require = require |
| local protoc = require("protoc") |
| local pb = require("pb") |
| local pb_encode = pb.encode |
| |
| assert(protoc:load [[ |
| syntax = "proto3"; |
| message IntStringPattern { |
| int64 value = 1; |
| }]]) |
| |
| local patterns |
| do |
| local function G(pattern) |
| return {pattern, true} |
| end |
| |
| local function B(pattern) |
| return {pattern, [[bad argument #2 to '?' (number/'#number' expected for field 'value', got string)]]} |
| end |
| |
| patterns = { |
| G(1), G(2), G(-3), G("#123"), G("0xabF"), G("#-0x123abcdef"), G("-#0x123abcdef"), G("#0x123abcdef"), G("123"), |
| B("#a"), B("+aaa"), B("#aaaa"), B("#-aa"), |
| } |
| end |
| |
| for _, p in pairs(patterns) do |
| local pattern = { |
| value = p[1], |
| } |
| local status, err = pcall(pb_encode, "IntStringPattern", pattern) |
| local res = status |
| if not res then |
| res = err |
| end |
| assert(res == p[2]) |
| end |
| ngx.say("passed") |
| } |
| } |
| --- response_body |
| passed |
| |
| |
| |
| === TEST 14: pb_option - check the matchings between enum and category |
| --- config |
| location /t { |
| content_by_lua_block { |
| local ngx_re_match = ngx.re.match |
| local plugin = require("apisix.plugins.grpc-transcode") |
| |
| local pb_option_def = plugin.schema.properties.pb_option.items.anyOf |
| |
| local patterns = { |
| [[^enum_as_.+$]], |
| [[^int64_as_.+$]], |
| [[^.+_default_.+$]], |
| [[^.+_hooks$]], |
| } |
| |
| local function check_pb_option_enum_category() |
| for i, category in ipairs(pb_option_def) do |
| for _, enum in ipairs(category.enum) do |
| if not ngx_re_match(enum, patterns[i], "jo") then |
| return ([[mismatch between enum("%s") and category("%s")]]):format( |
| enum, category.description) |
| end |
| end |
| end |
| end |
| |
| local err = check_pb_option_enum_category() |
| if err then |
| ngx.say(err) |
| return |
| end |
| |
| ngx.say("done") |
| } |
| } |
| --- request |
| GET /t |
| --- response_body |
| done |