Browse Source

Build test

remotes/github/main
Дмитрий 3 years ago
parent
commit
3a68ec9738
  1. 2
      .github/workflows/runtests.yml
  2. 69
      Makefile
  3. 110
      cmd/client_server/client.go
  4. 133
      cmd/client_server/handlers.go
  5. 86
      cmd/client_server/handlers_test.go
  6. 17
      cmd/client_server/server.go
  7. 67
      cmd/client_server/validators.go
  8. 82
      cmd/client_server/validators_test.go
  9. 6
      cmd/custom_types/partners_address.go
  10. 59
      cmd/main.go
  11. 31
      cmd/requests_types/requests_types.go
  12. 16
      cmd/requests_types/response_type.go
  13. 3
      go.mod
  14. 13
      internal/curl_requests/main.sh
  15. 29
      internal/curl_requests/no_resp_for_imp.json
  16. 19
      internal/curl_requests/simple.json
  17. 19
      internal/curl_requests/skip.json
  18. 29
      internal/json/valid_response.json
  19. 37
      internal/moc_server.go

2
.github/workflows/runtests.yml

@ -39,7 +39,7 @@ jobs:
# your code must be built into builds/ssp binary # your code must be built into builds/ssp binary
- name: Build SSP binary - name: Build SSP binary
run: echo Ain\'t no ssp yet run: go build -o builds/ssp cmd/main.go
#- name: Making it executable #- name: Making it executable
# run: chmod +x builds/ssp # run: chmod +x builds/ssp

69
Makefile

@ -0,0 +1,69 @@
# port for main server
port := 5053
moc_server_address := 127.0.0.1:5059,127.0.0.1:5058
bold := \033[1m
normal := \033[0m
good := \033[1m\033[0;32m
help:
@echo "$(bold)Makefile commands$(normal)"
@echo "-----------------"
@echo "$(bold)make build$(normal) : will build the project"
@echo "$(bold)make tests$(normal) : run tests for the project"
@echo "$(bold)make run$(normal) : will run the project"
@echo ""
@echo "$(bold)OS commands$(normal)"
@echo "-----------"
@echo "start server at PORT with 'IP:PORT' list of partners:"
@echo "$(bold)./bin/simple-choose-ad -p PORT -d 'IP:PORT'$(normal)"
run:
go run cmd/main.go -p $(port) -d "$(moc_server_address)"
test-ip:
@echo
@go run cmd/main.go -p $(port) -d "$(moc_server_address),localhost:5059" || \
{ echo "\n[+] PASS wrong IP address test"; exit 0; }
test-port:
@echo
@go run cmd/main.go -p $(port) -d "$(moc_server_address),127.0.0.1:as" || \
{ echo "\n[+] PASS wrong port test"; exit 0; }
test-port-max:
@echo
@go run cmd/main.go -p $(port) -d "$(moc_server_address),127.0.0.1:65537" || \
{ echo "\n[+] PASS port too big test"; exit 0; }
test-port-endpoint:
@echo
@go run cmd/main.go -p $(port) -d "127.0.0.1:9001/bid_request" || \
{ echo "\n[+] PASS endpoint with address test"; exit 0; }
build:
go build -o bin/simple-choose-ad cmd/main.go
start-moc-server:
@echo "[!] Starting up moc-server on $(moc_server_address) ..."
@go run internal/moc_server.go -l $(moc_server_address) &
stop-moc-server:
@echo "[!] Stopping moc-server ..."
@curl -s -o /dev/null "$(moc_server_address)/exit" &
test-server:
@echo
@echo "Check response from moc-server "
@$(MAKE) start-moc-server
@cd "cmd/client_server/"; \
go test -v
@$(MAKE) stop-moc-server
tests:
# @$(MAKE) test-ip
# @$(MAKE) test-port
# @$(MAKE) test-port-max
# @$(MAKE) test-port-endpoint
@$(MAKE) test-server

110
cmd/client_server/client.go

@ -0,0 +1,110 @@
package clientserver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
req_types "sample-choose-ad/cmd/requests_types"
"sort"
"time"
)
func sendRequest(url string, body *[]byte) (req_types.SuccesResponse, error) {
var pResp req_types.SuccesResponse
c := &http.Client{
Timeout: 200 * time.Millisecond,
}
resp, err := c.Post(url, "application/json", bytes.NewReader(*body))
if err != nil {
eText := fmt.Sprintf("Error: partner %v not responding", url)
return pResp, errors.New(eText)
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return pResp, errors.New("No content")
}
b, _ := ioutil.ReadAll(resp.Body)
if json.Unmarshal(b, &pResp) != nil {
log.Println(err)
}
return pResp, nil
}
// Create requset body based in incoming reqest `ir` and return
// `OutgoingRequest` as []byte from marshaled JSON
func constructPartnersRequestBody(ir *req_types.IncomingRequest) []byte {
var outReqBody req_types.OutgoingRequest
var imps []req_types.Imp
// WARN: uint and float multiplication may cause problems
for _, tile := range ir.Tiles {
imps = append(imps, req_types.Imp{
Id: tile.Id,
Minwidth: tile.Width,
Minheight: uint(math.Floor(float64(tile.Width * uint(tile.Ratio))))})
}
outReqBody.Id = *ir.Id
outReqBody.Imp = imps
outReqBody.Context = ir.Context
t, _ := json.Marshal(outReqBody)
return t
}
// map[imp.id]map[imp.id.price]
type PartnersResponses map[uint]map[float64]req_types.RespImp
// Make request for each partner and returns
func makePartnersRequests(partners []customtypes.PartnersAddress, ir *req_types.IncomingRequest) {
p_body := constructPartnersRequestBody(ir)
// Two data structures:
// partnersRespones for getting price with O(1) complexity
// []prices as slice of actual prices
partnersRespones := make(map[uint]map[float64]req_types.RespImp)
prices := make(map[uint][]float64)
for _, p := range partners {
url := fmt.Sprintf("http://%v:%v/%v", p.Ip, p.Port, PARTNER_ENDPOINT)
re, err := sendRequest(url, &p_body)
if err != nil {
log.Println(err)
continue
}
// append only successful responses
for _, r := range re.Imp {
if partnersRespones[r.Id] == nil {
partnersRespones[r.Id] = make(map[float64]req_types.RespImp)
}
partnersRespones[r.Id][r.Price] = r
prices[r.Id] = append(prices[r.Id], r.Price)
}
}
if len(partnersRespones) == 0 {
log.Println("Error: no responses from partners.")
return
}
// Sorting prices, now biggest price at index len-1
for _, p := range prices {
sort.Float64s(p)
}
}

133
cmd/client_server/handlers.go

@ -0,0 +1,133 @@
package clientserver
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
req_types "sample-choose-ad/cmd/requests_types"
"sort"
)
const PARTNER_ENDPOINT = "bid_request"
// Parsing and checking incoming request.
func parseAndCheckIncomingRequest(w http.ResponseWriter, r *http.Request) (req_types.IncomingRequest, error) {
var inpReqBody req_types.IncomingRequest
var err error
//check request method. Only POST valid.
if r.Method == "GET" {
w.WriteHeader(http.StatusBadRequest)
return inpReqBody, errors.New("Wrong request method")
}
// Check if body in incoming request is empty
body, _ := ioutil.ReadAll(r.Body)
if json.Unmarshal(body, &inpReqBody) != nil {
log.Println("Unmarshaling problem", string(body))
return inpReqBody, throwHTTPError("WRONG_SCHEMA", 400, &w)
}
// Check if Id is empty
if inpReqBody.Id == nil {
return inpReqBody, throwHTTPError("EMPTY_FIELD", 400, &w)
}
// Check if tiles is empty
if len(inpReqBody.Tiles) == 0 {
return inpReqBody, throwHTTPError("EMPTY_TILES", 400, &w)
}
// ipv4 validation
if wrongIPAddresFormat(inpReqBody.Context.Ip) {
return inpReqBody, throwHTTPError("WRONG_SCHEMA", 400, &w)
}
return inpReqBody, err
}
// Request handler with wrapper (make request for each partner in `[]partners`).
func handleRequest(partners []customtypes.PartnersAddress) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse incoming request and return an error, if it's empty
// or contains wrong/empty fields
incReq, err := parseAndCheckIncomingRequest(w, r)
if err != nil {
log.Println(err)
return
}
p_body := constructPartnersRequestBody(&incReq)
// Two data structures:
// partnersRespones for getting price with O(1) complexity
// []prices as slice of actual prices
partnersRespones := make(PartnersResponses)
prices := make(map[uint][]float64)
for _, p := range partners {
url := fmt.Sprintf("http://%v:%v/%v", p.Ip, p.Port, PARTNER_ENDPOINT)
re, err := sendRequest(url, &p_body)
if err != nil {
log.Println(err)
continue
}
// append only successful responses
for _, r := range re.Imp {
if partnersRespones[r.Id] == nil {
partnersRespones[r.Id] = make(map[float64]req_types.RespImp)
}
partnersRespones[r.Id][r.Price] = r
prices[r.Id] = append(prices[r.Id], r.Price)
}
}
if len(partnersRespones) == 0 {
log.Println("Error: no responses from partners.")
return
}
// Sorting prices, now biggest price at index len-1
for _, p := range prices {
sort.Float64s(p)
}
var bestOptions []req_types.RespImp
// for each tile peak best price
for _, tile := range incReq.Tiles {
if len(prices[tile.Id]) == 0 {
log.Println("No imp for tile ", tile.Id)
continue
}
last := len(prices[tile.Id]) - 1
biggestPrice := prices[tile.Id][last]
bestOptions = append(bestOptions, partnersRespones[tile.Id][biggestPrice])
}
response := req_types.SuccesResponse{
Id: *incReq.Id,
Imp: bestOptions,
}
respJSON, err := json.Marshal(response)
if err != nil {
log.Println(err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(respJSON)
}
}

86
cmd/client_server/handlers_test.go

@ -0,0 +1,86 @@
package clientserver
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
customtypes "sample-choose-ad/cmd/custom_types"
req_types "sample-choose-ad/cmd/requests_types"
"testing"
)
func TestGetRequestWithEmptyBody(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/placements/request", nil)
w := httptest.NewRecorder()
_, _ = req, w
a := handleRequest([]customtypes.PartnersAddress{{Ip: "127.0.0.1", Port: 5050}})
a(w, req)
res := w.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusBadRequest {
t.Errorf("Expects code 400, got %v", res.StatusCode)
}
}
func TestPostRequestWithEmptyBody(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/placements/request", nil)
w := httptest.NewRecorder()
_, _ = req, w
a := handleRequest([]customtypes.PartnersAddress{{Ip: "127.0.0.1", Port: 5050}})
a(w, req)
res := w.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusBadRequest {
t.Errorf("Expects code 400, got %v", res.StatusCode)
}
}
func TestPostRequestWithRightBody(t *testing.T) {
body_json := `{
"id": "123",
"tiles": [
{
"id": 123,
"width": 122,
"ratio": 1.5
}
],
"context": {
"ip": "192.168.1.1",
"user_agent": "curl"
}
}`
req := httptest.NewRequest(http.MethodPost, "/placements/request", bytes.NewBuffer([]byte(body_json)))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
_, _ = req, w
a := handleRequest([]customtypes.PartnersAddress{{Ip: "127.0.0.1", Port: 5059}})
a(w, req)
res := w.Result()
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Errorf("expected error to be nil got %v", err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expects code 200, got %v", res.StatusCode)
}
var d req_types.SuccesResponse
if json.Unmarshal(data, &d) != nil {
t.Log("Error parsing json response")
}
if d.Imp[0].Title != "bestoption" {
t.Errorf("Wants title `bestoption`, got %v", d.Imp[0].Title)
}
}

17
cmd/client_server/server.go

@ -0,0 +1,17 @@
package clientserver
import (
"fmt"
"log"
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
)
func StartServer(port string, partners []customtypes.PartnersAddress) {
http.HandleFunc("/placements/request", handleRequest(partners))
// http.HandleFunc("/placements/request", decorate(test2))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))
}

67
cmd/client_server/validators.go

@ -0,0 +1,67 @@
package clientserver
import (
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
)
const (
MAX_PORT_NUM = 65535
MIN_PORT_NUM = 1024
)
// Returns false if ipv4 `correct`.
func wrongIPAddresFormat(ipv4 string) bool {
re, err := regexp.Compile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`)
if err != nil {
log.Println(err)
}
return !re.Match([]byte(ipv4))
}
func throwHTTPError(err_text string, code int, w *http.ResponseWriter) error {
http.Error(*w, err_text, code)
eText := fmt.Sprintf("Error: %d %vr", code, err_text)
return errors.New(eText)
}
// Wait string in format "10.10.10.10:8080", where `10.10.10.10` IPv4,
// and `8080` port. If ip or port has wrong format, returns error.
func ParsePartnersAddress(ipAndPort string) (string, int64, error) {
var err error
var ip string
var port int64
iap := strings.Split(ipAndPort, ":")
if len(iap) != 2 {
err = errors.New(fmt.Sprintf("Wrong partners 'ip:port' format: %v", ipAndPort))
return ip, port, err
}
ip = iap[0]
if wrongIPAddresFormat(ip) {
err = errors.New(fmt.Sprintf("Wrong ip address format in partner ip: %v", ip))
}
port, e := strconv.ParseInt(iap[1], 10, 64)
if e != nil {
err = errors.New(fmt.Sprintf("Wrong port format in partner ip: %v", e))
return ip, port, err
}
if port > MAX_PORT_NUM {
err = errors.New(fmt.Sprintf("Wrong port in partner ip: grater than %v", MAX_PORT_NUM))
}
if port < MIN_PORT_NUM {
err = errors.New(fmt.Sprintf("Wrong port in partner ip: %v lower than %v", port, MIN_PORT_NUM))
}
return ip, port, err
}

82
cmd/client_server/validators_test.go

@ -0,0 +1,82 @@
package clientserver
import (
"testing"
)
// Wants: 10.10.10.10:5050
// Gets : localhost:5050
func TestIPAddresFormat_DomainName(t *testing.T) {
addres := "localhost:5050"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10:
func TestIPAddresFormat_OnlyIpAndColon(t *testing.T) {
addres := "10.10.10.10:"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10
func TestIPAddresFormat_OnlyIp(t *testing.T) {
addres := "10.10.10.10"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10:65537
func TestIPAddresFormat_IncorrectPortValue_TooBig(t *testing.T) {
addres := "10.10.10.10:65537"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10:1000
func TestIPAddresFormat_IncorrectPortValue_TooSmall(t *testing.T) {
addres := "10.10.10.10:1000"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10:as
func TestIPAddresFormat_IncorrectPortValue_NotANumber(t *testing.T) {
addres := "10.10.10.10:as"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}
// Wants: 10.10.10.10:5050
// Gets : 10.10.10.10:5050/bid_request
func TestIPAddresFormat_AddressWithEndpoint(t *testing.T) {
addres := "10.10.10.10:5050/bid_request"
_, _, e := ParsePartnersAddress(addres)
if e == nil {
t.Error("Must be an error, when parsing ", addres)
}
t.Log(e)
}

6
cmd/custom_types/partners_address.go

@ -0,0 +1,6 @@
package customtypes
type PartnersAddress struct {
Ip string
Port int64
}

59
cmd/main.go

@ -0,0 +1,59 @@
/*
Usage:
sample-choose-ad [flags]
The flags are:
-p
Listening port
-d
Adversment partners list in format ip_p1:port,ip_p2:port2...ip_p10:port
*/
package main
import (
"flag"
"log"
clientserver "sample-choose-ad/cmd/client_server"
customtypes "sample-choose-ad/cmd/custom_types"
"strings"
)
func main() {
log.Println("Info: Starting server")
port := flag.String("p", "", "-p 5050")
addressesList := flag.String("d", "", "-d '10.10.10.10:5050,10.10.10.20:5050'")
flag.Parse()
if *port == "" {
log.Fatalln("Error: Port number is require!")
}
if *addressesList == "" {
log.Fatalln("Error: Partners list is require!")
}
// Parse first 10 ip:port pairs into `[]partners` slise
var partners []customtypes.PartnersAddress
for i, p := range strings.Split(*addressesList, ",") {
if i == 10 {
log.Println("Warning: Partners count must be less or equal 10!")
return
}
ip, port, err := clientserver.ParsePartnersAddress(p)
if err != nil {
log.Fatalln(err)
}
partners = append(partners, customtypes.PartnersAddress{
Ip: ip,
Port: port})
}
clientserver.StartServer(*port, partners)
}

31
cmd/requests_types/requests_types.go

@ -0,0 +1,31 @@
package req_types
type Tile struct {
Id uint `json:"id"`
Width uint `json:"width"`
Ratio float64 `json:"ratio"`
}
type AdContext struct {
Ip string `json:"ip"`
UserAgent string `json:"user_agent"`
}
type IncomingRequest struct {
Id *string `json:"id"`
Tiles []Tile `json:"tiles"`
Context AdContext `json:"context"`
}
// Based in Tile
type Imp struct {
Id uint `json:"id"` // same as related `Tile.Id`
Minwidth uint `json:"minwidth"` // `Tile.Width`
Minheight uint `json:"minheight"` // math.Floor(Tile.Width * Tile.Ratio)
}
type OutgoingRequest struct {
Id string `json:"id"`
Imp []Imp `json:"imp"`
Context AdContext `json:"context"`
}

16
cmd/requests_types/response_type.go

@ -0,0 +1,16 @@
package req_types
type RespImp struct {
Id uint `json:"id"`
Width uint `json:"width"`
Height uint `json:"height"`
Title string `json:"title"`
Url string `json:"url"`
Price float64 `json:"price,string"`
}
// Response from ad partners
type SuccesResponse struct {
Id string `json:"id"`
Imp []RespImp `json:"imp"`
}

3
go.mod

@ -0,0 +1,3 @@
module sample-choose-ad
go 1.18

13
internal/curl_requests/main.sh

@ -0,0 +1,13 @@
#!/bin/zsh
P=$(pwd)
curl -X POST http://127.0.0.1:5053/placements/request \
-H "Content-Type: application/json" \
-d @simple.json
curl -X POST http://127.0.0.1:5053/placements/request \
-H "Content-Type: application/json" \
-d @skip.json
curl -X POST http://127.0.0.1:5053/placements/request \
-H "Content-Type: application/json" \
-d @no_resp_for_imp.json

29
internal/curl_requests/no_resp_for_imp.json

@ -0,0 +1,29 @@
{
"id": "test",
"tiles": [
{
"id": 554,
"width": 100,
"ratio": 1.5
},
{
"id": 555,
"width": 300,
"ratio": 1.5
},
{
"id": 556,
"width": 300,
"ratio": 2.5
},
{
"id": 400,
"width": 100,
"ratio": 2.5
}
],
"context": {
"ip": "192.168.1.1",
"user_agent": "curl"
}
}

19
internal/curl_requests/simple.json

@ -0,0 +1,19 @@
{
"id": "123",
"tiles": [
{
"id": 123,
"width": 100,
"ratio": 1.5
},
{
"id": 124,
"width": 300,
"ratio": 1.5
}
],
"context": {
"ip": "192.168.1.1",
"user_agent": "curl"
}
}

19
internal/curl_requests/skip.json

@ -0,0 +1,19 @@
{
"id": "skip",
"tiles": [
{
"id": 123,
"width": 100,
"ratio": 1.5
},
{
"id": 124,
"width": 300,
"ratio": 1.5
}
],
"context": {
"ip": "192.168.1.1",
"user_agent": "curl"
}
}

29
internal/json/valid_response.json

@ -0,0 +1,29 @@
{
"id": "123",
"imp": [
{
"id": 123,
"width": 144,
"height": 122,
"title": "example1",
"url": "example.com",
"price": 123.5
},
{
"id": 123,
"width": 155,
"height": 133,
"title": "bestoption",
"url": "bestoption.com",
"price": 143.8
},
{
"id": 123,
"width": 155,
"height": 133,
"title": "notabestoption",
"url": "notabestoption.com",
"price": 100.8
}
]
}

37
internal/moc_server.go

@ -0,0 +1,37 @@
package main
import (
"flag"
"log"
"net/http"
"os"
)
func main() {
file, err := os.ReadFile("internal/json/valid_response.json")
if err != nil {
log.Fatalln(err)
}
addr := flag.String("l", "", "-l 127.0.0.1:5059")
flag.Parse()
if *addr == "" {
log.Fatalln("Error: listening address is required!")
}
http.HandleFunc("/bid_request", func(w http.ResponseWriter, r *http.Request) {
// b, _ := ioutil.ReadAll(r.Body)
// log.Println(string(b))
w.Header().Add("Content-Type", "application/json")
w.Write(file)
})
// endpoint: /exit
// Terminate server with code 0.
http.HandleFunc("/exit", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
os.Exit(0)
})
log.Fatal(http.ListenAndServe(*addr, nil))
}
Loading…
Cancel
Save