Skip to content
This repository was archived by the owner on Sep 15, 2021. It is now read-only.
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ package interoptestservice

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -177,3 +183,139 @@ func (s *ServiceImpl) Run(ctx context.Context, req *interop.InteropRunRequest) (
func verifySpans(map[*commonpb.Node][]*tracepb.Span) {
// TODO: implement this
}

var _ http.Handler = (*ServiceImpl)(nil)

// ServeHTTP allows ServiceImpl to handle HTTP requests.
func (s *ServiceImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
// For options, unconditionally respond with a 200 and send CORS headers.
// Without properly responding to OPTIONS and without CORS headers, browsers
// won't be able to use this handler.
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
w.WriteHeader(200)
return
}

// Handle routing.
switch r.Method {
case "GET":
s.handleHTTPGET(w, r)
return

case "POST":
s.handleHTTPPOST(w, r)
return

default:
http.Error(w, "Unhandled HTTP Method: "+r.Method+" only accepting POST and GET", http.StatusMethodNotAllowed)
return
}
}

func deserializeJSON(blob []byte, save interface{}) error {
return json.Unmarshal(blob, save)
}

func serializeJSON(src interface{}) ([]byte, error) {
return json.Marshal(src)
}

// readTillEOFAndDeserializeJSON reads the entire body out of rc and then closes it.
// If it encounters an error, it will return it immediately.
// After successfully reading the body, it then JSON unmarshals to save.
func readTillEOFAndDeserializeJSON(rc io.ReadCloser, save interface{}) error {
// We are always receiving an interop.InteropResultRequest
blob, err := ioutil.ReadAll(rc)
_ = rc.Close()
if err != nil {
return err
}
return deserializeJSON(blob, save)
}

const resultPathPrefix = "/result/"

func (s *ServiceImpl) handleHTTPGET(w http.ResponseWriter, r *http.Request) {
// Expecting a request path of: "/result/:id"
var path string
if r.URL != nil {
path = r.URL.Path
}

if len(path) <= len(resultPathPrefix) {
http.Error(w, "Expected path of the form: /result/:id", http.StatusBadRequest)
return
}

strId := strings.TrimPrefix(path, resultPathPrefix)
if strId == "" || strId == "/" {
http.Error(w, "Expected path of the form: /result/:id", http.StatusBadRequest)
return
}

id, err := strconv.ParseInt(strId, 10, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// TODO: actually look up the available tests by their IDs
req := &interop.InteropResultRequest{Id: id}
res, err := s.Result(r.Context(), req)
if err != nil {
// TODO: perhaps multiplex on NotFound and other sentinel errors.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

blob, _ := serializeJSON(res)
w.Header().Set("Content-Type", "application/json")
w.Write(blob)
}

func (s *ServiceImpl) handleHTTPPOST(w http.ResponseWriter, r *http.Request) {
var path string
if r.URL != nil {
path = r.URL.Path
}

ctx := r.Context()
var res interface{}
var err error

switch path {
case "/run", "/run/":
inrreq := new(interop.InteropRunRequest)
if err := readTillEOFAndDeserializeJSON(r.Body, inrreq); err != nil {
http.Error(w, "Failed to JSON unmarshal interop.InteropRunRequest: "+err.Error(), http.StatusBadRequest)
return
}
res, err = s.Run(ctx, inrreq)

case "/result", "/result/":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between POST /result and GET /result?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • GET /result is to retrieve the results by ID or later for listing all the results
  • POST /result submits a single result

and as per the PR there are two different methods handleHTTPGET and handleHTTPPOST

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was GET /result and POST /result do the same thing. They both retrieve result. Why is there a need for POST /result ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was GET /result and POST /result do the same thing. They both retrieve result. Why is there a need for POST /result ?

Am a little confused by your the ask of this issue then: A POST method is used to upload content, a GET is used to retrieve it. https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods my apologies if am perhaps preaching to the choir.

POST was coded to be able to receive test results from whoever is running it e.g. a sharded test runner; GET to actually retrieve them(following the HTTP conventions)

Did you just want only functionality of a GET?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coordinator is computing results based on test it requests to test-services (javaservice, etc) and traces it receives from OC Agent. So no entity is actually posting any result to Test coordinator.

Also, the code for POST /result is actually reading the request from the body and returning the results. It isn't uploading any content.

inrreq := new(interop.InteropResultRequest)
if err := readTillEOFAndDeserializeJSON(r.Body, inrreq); err != nil {
http.Error(w, "Failed to JSON unmarshal interop.InteropResultRequest: "+err.Error(), http.StatusBadRequest)
return
}
res, err = s.Result(ctx, inrreq)

default:
http.Error(w, "Unmatched route: "+path+"\nOnly accepting /result and /run", http.StatusNotFound)
return
}

if err != nil {
// TODO: Perhap return a structured error e.g. {"error": <ERROR_MESSAGE>}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Otherwise all clear to return the response.
blob, _ := serializeJSON(res)
w.Header().Set("Content-Type", "application/json")
w.Write(blob)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2018, OpenCensus Authors
//
// Licensed 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.

package interoptestservice_test

import (
"bufio"
"net/http"
"net/http/httptest"
"net/http/httputil"
"strings"
"testing"

"github.com/census-ecosystem/opencensus-experiments/interoptest/src/testcoordinator/interoptestservice"
)

func TestRequestsOverHTTP(t *testing.T) {
h := new(interoptestservice.ServiceImpl)
tests := []struct {
reqWire string // The request's wire data
wantResWire string // The response's wire format
}{

// OPTIONS request
{
reqWire: `OPTIONS / HTTP/1.1
Host: *

`,
wantResWire: "HTTP/1.1 200 OK\r\n" +
"Connection: close\r\n" +
"Access-Control-Allow-Headers: *\r\n" +
"Access-Control-Allow-Methods: *\r\n" +
"Access-Control-Allow-Origin: *\r\n\r\n",
},

// GET: bad path
{
reqWire: `GET / HTTP/1.1
Host: foo
Content-Length: 0

`,
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
"Connection: close\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"X-Content-Type-Options: nosniff\r\n\r\n" +
"Expected path of the form: /result/:id\n",
},

// GET: good path no id
{
reqWire: `GET /result HTTP/1.1

`,
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
"Connection: close\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"X-Content-Type-Options: nosniff\r\n\r\n" +
"Expected path of the form: /result/:id\n",
},

// GET: good path with proper id
{
reqWire: `GET /result/1 HTTP/1.1

`,
wantResWire: "HTTP/1.1 200 OK\r\n" +
"Connection: close\r\n" +
"Content-Type: application/json\r\n\r\n" +
`{"id":1,"status":{}}`,
},
// POST: no body
{
reqWire: `POST /result HTTP/1.1

`,
wantResWire: "HTTP/1.1 400 Bad Request\r\n" +
"Connection: close\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"X-Content-Type-Options: nosniff\r\n\r\n" +
"Failed to JSON unmarshal interop.InteropResultRequest: unexpected end of JSON input\n",
},

// POST: body with content length to accepted route
{
reqWire: `POST /result HTTP/1.1
Content-Length: 9
Content-Type: application/json

{"id":10}
`,
wantResWire: "HTTP/1.1 200 OK\r\n" +
"Connection: close\r\n" +
"Content-Type: application/json\r\n\r\n" +
`{"id":10,"status":{}}`,
},

// POST: body with no content length
{
// Using a string concatenation here because for "streamed"/"chunked"
// requests, we have to ensure that the last 2 bytes before EOF are
// strictly "\r\n" lest a "malformed chunked encoding" error.
reqWire: "POST /result HTTP/1.1\r\n" +
"Host: golang.org\r\n" +
"Content-Type: application/json\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Accept-Encoding: gzip\r\n\r\n" +
"b\r\n" +
"{\"id\":8888}\r\n" +
"0\r\n\r\n",
wantResWire: "HTTP/1.1 200 OK\r\n" +
"Connection: close\r\n" +
"Content-Type: application/json\r\n\r\n" +
`{"id":8888,"status":{}}`,
},

// POST: body with content length to non-existent route
{
reqWire: `POST /results HTTP/1.1
Content-Length: 9
Content-Type: application/json

{"id":10}
`,
wantResWire: "HTTP/1.1 404 Not Found\r\n" +
"Connection: close\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"X-Content-Type-Options: nosniff\r\n\r\n" +
"Unmatched route: /results\n" +
"Only accepting /result and /run\n",
},
}

for i, tt := range tests {
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(tt.reqWire)))
if err != nil {
t.Errorf("#%d unexpected error parsing request: %v", i, err)
continue
}

rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
gotResBlob, _ := httputil.DumpResponse(rec.Result(), true)
gotRes := string(gotResBlob)
if gotRes != tt.wantResWire {
t.Errorf("#%d non-matching responses\nGot:\n%q\nWant:\n%q", i, gotRes, tt.wantResWire)
}
}
}