Skip to content
This repository was archived by the owner on Sep 15, 2021. It is now read-only.

Commit 7ff9403

Browse files
committed
interoptestservice: support GET and POST HTTP requests
Support for requests over HTTP with: * GET to /result/:id which will retrieve a test result. As shown in the template route, ":id" must be replaced with the actual id which is a 64-bit signed integer * POST to /result and /run which will For /result submit a result For /run, run the test asynchronously Fixes #81
1 parent c577fff commit 7ff9403

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed

interoptest/src/testcoordinator/interoptestservice/interop_test_service.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ package interoptestservice
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"errors"
2021
"fmt"
22+
"io"
23+
"io/ioutil"
2124
"math/rand"
2225
"net"
26+
"net/http"
27+
"strconv"
28+
"strings"
2329
"sync"
2430
"time"
2531

@@ -177,3 +183,139 @@ func (s *ServiceImpl) Run(ctx context.Context, req *interop.InteropRunRequest) (
177183
func verifySpans(map[*commonpb.Node][]*tracepb.Span) {
178184
// TODO: implement this
179185
}
186+
187+
var _ http.Handler = (*ServiceImpl)(nil)
188+
189+
// ServeHTTP allows ServiceImpl to handle HTTP requests.
190+
func (s *ServiceImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
191+
if r.Method == "OPTIONS" {
192+
// For options, unconditionally respond with a 200 and send CORS headers.
193+
// Without properly responding to OPTIONS and without CORS headers, browsers
194+
// won't be able to use this handler.
195+
w.Header().Add("Access-Control-Allow-Origin", "*")
196+
w.Header().Add("Access-Control-Allow-Methods", "*")
197+
w.Header().Add("Access-Control-Allow-Headers", "*")
198+
w.WriteHeader(200)
199+
return
200+
}
201+
202+
// Handle routing.
203+
switch r.Method {
204+
case "GET":
205+
s.handleHTTPGET(w, r)
206+
return
207+
208+
case "POST":
209+
s.handleHTTPPOST(w, r)
210+
return
211+
212+
default:
213+
http.Error(w, "Unhandled HTTP Method: "+r.Method+" only accepting POST and GET", http.StatusMethodNotAllowed)
214+
return
215+
}
216+
}
217+
218+
func deserializeJSON(blob []byte, save interface{}) error {
219+
return json.Unmarshal(blob, save)
220+
}
221+
222+
func serializeJSON(src interface{}) ([]byte, error) {
223+
return json.Marshal(src)
224+
}
225+
226+
// readTillEOFAndDeserializeJSON reads the entire body out of rc and then closes it.
227+
// If it encounters an error, it will return it immediately.
228+
// After successfully reading the body, it then JSON unmarshals to save.
229+
func readTillEOFAndDeserializeJSON(rc io.ReadCloser, save interface{}) error {
230+
// We are always receiving an interop.InteropResultRequest
231+
blob, err := ioutil.ReadAll(rc)
232+
_ = rc.Close()
233+
if err != nil {
234+
return err
235+
}
236+
return deserializeJSON(blob, save)
237+
}
238+
239+
const resultPathPrefix = "/result/"
240+
241+
func (s *ServiceImpl) handleHTTPGET(w http.ResponseWriter, r *http.Request) {
242+
// Expecting a request path of: "/result/:id"
243+
var path string
244+
if r.URL != nil {
245+
path = r.URL.Path
246+
}
247+
248+
if len(path) <= len(resultPathPrefix) {
249+
http.Error(w, "Expected path of the form: /result/:id", http.StatusBadRequest)
250+
return
251+
}
252+
253+
strId := strings.TrimPrefix(path, resultPathPrefix)
254+
if strId == "" || strId == "/" {
255+
http.Error(w, "Expected path of the form: /result/:id", http.StatusBadRequest)
256+
return
257+
}
258+
259+
id, err := strconv.ParseInt(strId, 10, 64)
260+
if err != nil {
261+
http.Error(w, err.Error(), http.StatusBadRequest)
262+
return
263+
}
264+
265+
// TODO: actually look up the available tests by their IDs
266+
req := &interop.InteropResultRequest{Id: id}
267+
res, err := s.Result(r.Context(), req)
268+
if err != nil {
269+
// TODO: perhaps multiplex on NotFound and other sentinel errors.
270+
http.Error(w, err.Error(), http.StatusInternalServerError)
271+
return
272+
}
273+
274+
blob, _ := serializeJSON(res)
275+
w.Header().Set("Content-Type", "application/json")
276+
w.Write(blob)
277+
}
278+
279+
func (s *ServiceImpl) handleHTTPPOST(w http.ResponseWriter, r *http.Request) {
280+
var path string
281+
if r.URL != nil {
282+
path = r.URL.Path
283+
}
284+
285+
ctx := r.Context()
286+
var res interface{}
287+
var err error
288+
289+
switch path {
290+
case "/run", "/run/":
291+
inrreq := new(interop.InteropRunRequest)
292+
if err := readTillEOFAndDeserializeJSON(r.Body, inrreq); err != nil {
293+
http.Error(w, "Failed to JSON unmarshal interop.InteropRunRequest: "+err.Error(), http.StatusBadRequest)
294+
return
295+
}
296+
res, err = s.Run(ctx, inrreq)
297+
298+
case "/result", "/result/":
299+
inrreq := new(interop.InteropResultRequest)
300+
if err := readTillEOFAndDeserializeJSON(r.Body, inrreq); err != nil {
301+
http.Error(w, "Failed to JSON unmarshal interop.InteropResultRequest: "+err.Error(), http.StatusBadRequest)
302+
return
303+
}
304+
res, err = s.Result(ctx, inrreq)
305+
306+
default:
307+
http.Error(w, "Unmatched route: "+path+"\nOnly accepting /result and /run", http.StatusNotFound)
308+
return
309+
}
310+
311+
if err != nil {
312+
// TODO: Perhap return a structured error e.g. {"error": <ERROR_MESSAGE>}
313+
http.Error(w, err.Error(), http.StatusInternalServerError)
314+
return
315+
}
316+
317+
// Otherwise all clear to return the response.
318+
blob, _ := serializeJSON(res)
319+
w.Header().Set("Content-Type", "application/json")
320+
w.Write(blob)
321+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2018, OpenCensus Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package interoptestservice_test
16+
17+
import (
18+
"bufio"
19+
"net/http"
20+
"net/http/httptest"
21+
"net/http/httputil"
22+
"strings"
23+
"testing"
24+
25+
"github.com/census-ecosystem/opencensus-experiments/interoptest/src/testcoordinator/interoptestservice"
26+
)
27+
28+
func TestRequestsOverHTTP(t *testing.T) {
29+
h := new(interoptestservice.ServiceImpl)
30+
tests := []struct {
31+
reqWire string // The request's wire data
32+
wantResWire string // The response's wire format
33+
}{
34+
35+
// OPTIONS request
36+
{
37+
reqWire: `OPTIONS / HTTP/1.1
38+
Host: *
39+
40+
`,
41+
wantResWire: "HTTP/1.1 200 OK\r\n" +
42+
"Connection: close\r\n" +
43+
"Access-Control-Allow-Headers: *\r\n" +
44+
"Access-Control-Allow-Methods: *\r\n" +
45+
"Access-Control-Allow-Origin: *\r\n\r\n",
46+
},
47+
48+
// GET: bad path
49+
{
50+
reqWire: `GET / HTTP/1.1
51+
Host: foo
52+
Content-Length: 0
53+
54+
`,
55+
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
56+
"Connection: close\r\n" +
57+
"Content-Type: text/plain; charset=utf-8\r\n" +
58+
"X-Content-Type-Options: nosniff\r\n\r\n" +
59+
"Expected path of the form: /result/:id\n",
60+
},
61+
62+
// GET: good path no id
63+
{
64+
reqWire: `GET /result HTTP/1.1
65+
66+
`,
67+
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
68+
"Connection: close\r\n" +
69+
"Content-Type: text/plain; charset=utf-8\r\n" +
70+
"X-Content-Type-Options: nosniff\r\n\r\n" +
71+
"Expected path of the form: /result/:id\n",
72+
},
73+
74+
// GET: good path with proper id
75+
{
76+
reqWire: `GET /result/1 HTTP/1.1
77+
78+
`,
79+
wantResWire: "HTTP/1.1 200 OK\r\n" +
80+
"Connection: close\r\n" +
81+
"Content-Type: application/json\r\n\r\n" +
82+
`{"id":1,"status":{}}`,
83+
},
84+
// POST: no body
85+
{
86+
reqWire: `POST /result HTTP/1.1
87+
88+
`,
89+
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
90+
"Connection: close\r\n" +
91+
"Content-Type: text/plain; charset=utf-8\r\n" +
92+
"X-Content-Type-Options: nosniff\r\n\r\n" +
93+
"Failed to JSON unmarshal interop.InteropResultRequest: unexpected end of JSON input\n",
94+
},
95+
96+
// POST: body with content length to accepted route
97+
{
98+
reqWire: `POST /result HTTP/1.1
99+
Content-Length: 9
100+
Content-Type: application/json
101+
102+
{"id":10}
103+
`,
104+
wantResWire: "HTTP/1.1 200 OK\r\n" +
105+
"Connection: close\r\n" +
106+
"Content-Type: application/json\r\n\r\n" +
107+
`{"id":10,"status":{}}`,
108+
},
109+
110+
// POST: body with no content length
111+
{
112+
// Using a string concatenation here because for "streamed"/"chunked"
113+
// requests, we have to ensure that the last 2 bytes before EOF are
114+
// strictly "\r\n" lest a "malformed chunked encoding" error.
115+
reqWire: "POST /result HTTP/1.1\r\n" +
116+
"Host: golang.org\r\n" +
117+
"Content-Type: application/json\r\n" +
118+
"Transfer-Encoding: chunked\r\n" +
119+
"Accept-Encoding: gzip\r\n\r\n" +
120+
"b\r\n" +
121+
"{\"id\":8888}\r\n" +
122+
"0\r\n\r\n",
123+
wantResWire: "HTTP/1.1 200 OK\r\n" +
124+
"Connection: close\r\n" +
125+
"Content-Type: application/json\r\n\r\n" +
126+
`{"id":8888,"status":{}}`,
127+
},
128+
129+
// POST: body with content length to non-existent route
130+
{
131+
reqWire: `POST /results HTTP/1.1
132+
Content-Length: 9
133+
Content-Type: application/json
134+
135+
{"id":10}
136+
`,
137+
wantResWire: "HTTP/1.1 404 Not Found\r\n" +
138+
"Connection: close\r\n" +
139+
"Content-Type: text/plain; charset=utf-8\r\n" +
140+
"X-Content-Type-Options: nosniff\r\n\r\n" +
141+
"Unmatched route: /results\n" +
142+
"Only accepting /result and /run\n",
143+
},
144+
}
145+
146+
for i, tt := range tests {
147+
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(tt.reqWire)))
148+
if err != nil {
149+
t.Errorf("#%d unexpected error parsing request: %v", i, err)
150+
continue
151+
}
152+
153+
rec := httptest.NewRecorder()
154+
h.ServeHTTP(rec, req)
155+
gotResBlob, _ := httputil.DumpResponse(rec.Result(), true)
156+
gotRes := string(gotResBlob)
157+
if gotRes != tt.wantResWire {
158+
t.Errorf("#%d non-matching responses\nGot:\n%q\nWant:\n%q", i, gotRes, tt.wantResWire)
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)