package databag import ( "bytes" "encoding/json" "errors" "github.com/gorilla/mux" "github.com/gorilla/websocket" "io/ioutil" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "time" ) const testReadDeadline = 2 const testRevisionWait = 100 type testCard struct { GUID string Token string CardID string GroupID string } type testContact struct { GUID string Token string Revisions chan *Revision A testCard B testCard C testCard D testCard } //TestGroup provides the setup accounts for a test case type TestGroup struct { A testContact B testContact C testContact D testContact } //GetTestRevision retrieves the notified revisions func GetTestRevision(status chan *Revision) (rev *Revision) { time.Sleep(testRevisionWait * time.Millisecond) for { select { case r := <-status: rev = r default: return } } } //APITestData is a helper function for testing asset download func APITestData( endpoint func(http.ResponseWriter, *http.Request), requestType string, name string, params *map[string]string, body interface{}, tokenType string, token string, start int64, end int64, ) (data []byte, hdr map[string][]string, err error) { var r *http.Request var w *httptest.ResponseRecorder if tokenType == APPTokenAgent { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "agent=" + token } else if tokenType == APPTokenContact { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "contact=" + token } if r, w, err = NewRequest(requestType, name, body); err != nil { return } if params != nil { r = mux.SetURLVars(r, *params) } if token != "" { r.Header.Add("TokenType", tokenType) SetBearerAuth(r, token) } if start != 0 || end != 0 { byteRange := "bytes=" + strconv.FormatInt(start, 10) + "-" + strconv.FormatInt(end, 10) r.Header.Add("Range", byteRange) } endpoint(w, r) resp := w.Result() if resp.StatusCode != 200 && resp.StatusCode != 206 { err = errors.New("response failed") return } hdr = resp.Header data, err = ioutil.ReadAll(resp.Body) return } //APITestMsg is a helper function for an endpoint test func APITestMsg( endpoint func(http.ResponseWriter, *http.Request), requestType string, name string, params *map[string]string, body interface{}, tokenType string, token string, response interface{}, responseHeader *map[string][]string, ) (err error) { var r *http.Request var w *httptest.ResponseRecorder if tokenType == APPTokenAgent { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "agent=" + token } else if tokenType == APPTokenContact { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "contact=" + token } if r, w, err = NewRequest(requestType, name, body); err != nil { return } if params != nil { r = mux.SetURLVars(r, *params) } if tokenType != "" { r.Header.Add("TokenType", tokenType) } if token != "" { SetBearerAuth(r, token) } endpoint(w, r) resp := w.Result() if resp.StatusCode != 200 { err = errors.New("response failed") return } if responseHeader != nil { *responseHeader = resp.Header } if response == nil { return } dec := json.NewDecoder(resp.Body) dec.Decode(response) return } //APITestUpload is a helper function for asset upload test func APITestUpload( endpoint func(http.ResponseWriter, *http.Request), requestType string, name string, params *map[string]string, body []byte, tokenType string, token string, response interface{}, responseHeader *map[string][]string, ) (err error) { data := bytes.Buffer{} writer := multipart.NewWriter(&data) part, err := writer.CreateFormFile("asset", "asset") if err != nil { return err } part.Write(body) if err = writer.Close(); err != nil { return } if tokenType == APPTokenAgent { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "agent=" + token } else if tokenType == APPTokenContact { if !strings.Contains(name, "?") { name += "?" } else { name += "&" } name += "contact=" + token } w := httptest.NewRecorder() r := httptest.NewRequest(requestType, name, &data) if params != nil { r = mux.SetURLVars(r, *params) } r.Header.Set("Content-Type", writer.FormDataContentType()) endpoint(w, r) resp := w.Result() if resp.StatusCode != 200 { err = errors.New("response failed") return } if responseHeader != nil { *responseHeader = resp.Header } if response == nil { return } dec := json.NewDecoder(resp.Body) dec.Decode(response) return } // // A --- connected,group connected,group --- B // | \ /| // | requested,nogroup confirmed,group | // | | // connected,group , // | // --x-- // | // connected,group , // | | // | , pending,nogroup | // |/ \| // C --- connected,group connected,group --- D // // AddTestGroup creates and connects test accounts for useCases func AddTestGroup(prefix string) (*TestGroup, error) { var err error var rev *Revision var ws *websocket.Conn // allocate contacts g := &TestGroup{} if g.A.GUID, g.A.Token, err = addTestAccount(prefix + "A"); err != nil { return g, err } if g.B.GUID, g.B.Token, err = addTestAccount(prefix + "B"); err != nil { return g, err } if g.C.GUID, g.C.Token, err = addTestAccount(prefix + "C"); err != nil { return g, err } if g.D.GUID, g.D.Token, err = addTestAccount(prefix + "D"); err != nil { return g, err } // setup A if g.A.B.CardID, err = addTestCard(g.A.Token, g.B.Token); err != nil { return g, err } if err = openTestCard(g.A.Token, g.A.B.CardID); err != nil { return g, err } if g.A.B.GroupID, err = groupTestCard(g.A.Token, g.A.B.CardID); err != nil { return g, err } if g.A.C.CardID, err = addTestCard(g.A.Token, g.C.Token); err != nil { return g, err } if err = openTestCard(g.A.Token, g.A.C.CardID); err != nil { return g, err } if g.A.C.GroupID, err = groupTestCard(g.A.Token, g.A.C.CardID); err != nil { return g, err } if g.A.D.CardID, err = addTestCard(g.A.Token, g.D.Token); err != nil { return g, err } if err = openTestCard(g.A.Token, g.A.D.CardID); err != nil { return g, err } // setup B if g.B.A.CardID, err = addTestCard(g.B.Token, g.A.Token); err != nil { return g, err } if err = openTestCard(g.B.Token, g.B.A.CardID); err != nil { return g, err } if g.B.A.GroupID, err = groupTestCard(g.B.Token, g.B.A.CardID); err != nil { return g, err } if g.B.C.CardID, err = addTestCard(g.B.Token, g.C.Token); err != nil { return g, err } if g.B.C.GroupID, err = groupTestCard(g.B.Token, g.B.C.CardID); err != nil { return g, err } // setup C if g.C.D.CardID, err = addTestCard(g.C.Token, g.D.Token); err != nil { return g, err } if err = openTestCard(g.C.Token, g.C.D.CardID); err != nil { return g, err } if g.C.D.GroupID, err = groupTestCard(g.C.Token, g.C.D.CardID); err != nil { return g, err } if g.C.A.CardID, err = addTestCard(g.C.Token, g.A.Token); err != nil { return g, err } if err = openTestCard(g.C.Token, g.C.A.CardID); err != nil { return g, err } if g.C.A.GroupID, err = groupTestCard(g.C.Token, g.C.A.CardID); err != nil { return g, err } // setup D if g.D.C.CardID, err = addTestCard(g.D.Token, g.C.Token); err != nil { return g, err } if err = openTestCard(g.D.Token, g.D.C.CardID); err != nil { return g, err } if g.D.C.GroupID, err = groupTestCard(g.D.Token, g.D.C.CardID); err != nil { return g, err } if g.D.A.CardID, err = getCardID(g.D.Token, g.A.GUID); err != nil { return g, err } // get contact tokens if g.A.B.Token, err = getCardToken(g.A.Token, g.A.B.CardID); err != nil { return g, err } if g.B.A.Token, err = getCardToken(g.B.Token, g.B.A.CardID); err != nil { return g, err } if g.C.A.Token, err = getCardToken(g.C.Token, g.C.A.CardID); err != nil { return g, err } if g.C.D.Token, err = getCardToken(g.C.Token, g.C.D.CardID); err != nil { return g, err } if g.D.C.Token, err = getCardToken(g.D.Token, g.D.C.CardID); err != nil { return g, err } // connect websockets rev = &Revision{} if ws, err = StatusConnection(g.A.Token, rev); err != nil { return g, err } g.A.Revisions = make(chan *Revision, 64) g.A.Revisions <- rev go monitorStatus(ws, &g.A) rev = &Revision{} if ws, err = StatusConnection(g.B.Token, rev); err != nil { return g, err } g.B.Revisions = make(chan *Revision, 64) g.B.Revisions <- rev go monitorStatus(ws, &g.B) rev = &Revision{} if ws, err = StatusConnection(g.C.Token, rev); err != nil { return g, err } g.C.Revisions = make(chan *Revision, 64) g.C.Revisions <- rev go monitorStatus(ws, &g.C) rev = &Revision{} if ws, err = StatusConnection(g.D.Token, rev); err != nil { return g, err } g.D.Revisions = make(chan *Revision, 64) g.D.Revisions <- rev go monitorStatus(ws, &g.D) return g, nil } func monitorStatus(ws *websocket.Conn, contact *testContact) { var data []byte var dataType int var err error // reset any timeout ws.SetReadDeadline(time.Time{}) // read revision update for { if dataType, data, err = ws.ReadMessage(); err != nil { LogMsg("failed to read status connection") return } if dataType != websocket.TextMessage { LogMsg("invalid status data type") return } rev := &Revision{} if err = json.Unmarshal(data, rev); err != nil { LogMsg("invalid status data") return } contact.Revisions <- rev } } func getCardToken(account string, cardID string) (token string, err error) { var r *http.Request var w *httptest.ResponseRecorder var cardDetail CardDetail var cardProfile CardProfile vars := make(map[string]string) vars["cardID"] = cardID if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/detail?agent="+account, nil); err != nil { return } r = mux.SetURLVars(r, vars) GetCardDetail(w, r) if err = ReadResponse(w, &cardDetail); err != nil { return } if cardDetail.Status != APPCardConnected { err = errors.New("card not connected") return } if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/profile?agent="+account, nil); err != nil { return } r = mux.SetURLVars(r, vars) GetCardProfile(w, r) if err = ReadResponse(w, &cardProfile); err != nil { return } token = cardProfile.GUID + "." + cardDetail.Token return } func getCardID(account string, guid string) (cardID string, err error) { var r *http.Request var w *httptest.ResponseRecorder var cards []Card if r, w, err = NewRequest("GET", "/contact/cards?agent="+account, nil); err != nil { return } GetCards(w, r) if err = ReadResponse(w, &cards); err != nil { return } for _, card := range cards { if card.Data.CardProfile.GUID == guid { cardID = card.ID return } } err = errors.New("card not found") return } func groupTestCard(account string, cardID string) (groupID string, err error) { var r *http.Request var w *httptest.ResponseRecorder var subject *Subject var group Group var cardData CardData vars := make(map[string]string) // add new group subject = &Subject{ DataType: "imagroup", Data: "group data with name and logo", } if r, w, err = NewRequest("POST", "/share/groups?agent="+account, subject); err != nil { return } AddGroup(w, r) if err = ReadResponse(w, &group); err != nil { return } groupID = group.ID // set contact group if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/groups/{groupID}?agent="+account, nil); err != nil { return } vars["groupID"] = group.ID vars["cardID"] = cardID r = mux.SetURLVars(r, vars) SetCardGroup(w, r) if err = ReadResponse(w, &cardData); err != nil { return } return } func openTestCard(account string, cardID string) (err error) { var r *http.Request var w *httptest.ResponseRecorder var msg DataMessage var card Card var vars = map[string]string{"cardID": cardID} var contactStatus ContactStatus // set to connecting state if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/status?agent="+account, APPCardConnecting); err != nil { return } r = mux.SetURLVars(r, vars) SetCardStatus(w, r) if err = ReadResponse(w, &card); err != nil { return } // get open message if r, w, err = NewRequest("GET", "/contact/cards/{cardID}/openMessage?agent="+account, nil); err != nil { return } r = mux.SetURLVars(r, vars) GetOpenMessage(w, r) if err = ReadResponse(w, &msg); err != nil { return } // set open message if r, w, err = NewRequest("PUT", "/contact/openMessage", msg); err != nil { return } SetOpenMessage(w, r) if err = ReadResponse(w, &contactStatus); err != nil { return } // update status if connected if contactStatus.Status == APPCardConnected { view := "viewRevision=" + strconv.FormatInt(contactStatus.ViewRevision, 10) article := "articleRevision=" + strconv.FormatInt(contactStatus.ArticleRevision, 10) channel := "channelRevision=" + strconv.FormatInt(contactStatus.ChannelRevision, 10) profile := "profileRevision=" + strconv.FormatInt(contactStatus.ProfileRevision, 10) if r, w, err = NewRequest("PUT", "/contact/cards/{cardID}/status?agent="+account+"&token="+contactStatus.Token+"&"+view+"&"+article+"&"+channel+"&"+profile, APPCardConnected); err != nil { return } r = mux.SetURLVars(r, vars) SetCardStatus(w, r) if err = ReadResponse(w, &card); err != nil { return } } return } func addTestCard(account string, contact string) (cardID string, err error) { var r *http.Request var w *httptest.ResponseRecorder var msg DataMessage var card Card // get A identity message if r, w, err = NewRequest("GET", "/profile/message?agent="+contact, nil); err != nil { return } GetProfileMessage(w, r) if err = ReadResponse(w, &msg); err != nil { return } // add A card in B if r, w, err = NewRequest("POST", "/contact/cards?agent="+account, &msg); err != nil { return } AddCard(w, r) if err = ReadResponse(w, &card); err != nil { return } cardID = card.ID return } func addTestAccount(username string) (guid string, token string, err error) { var r *http.Request var w *httptest.ResponseRecorder var access LoginAccess var claim Claim var msg DataMessage var profile Profile var login = username + ":pass" // admin login r, w, _ = NewRequest("PUT", "/admin/access?token=pass", nil); SetAdminAccess(w, r) var session string if ReadResponse(w, &session) != nil { panic("failed to login as admin") } // get account token if r, w, err = NewRequest("POST", "/admin/accounts?token=" + session, nil); err != nil { return } AddNodeAccount(w, r) if err = ReadResponse(w, &access); err != nil { return } // set account profile if r, w, err = NewRequest("POST", "/account/profile", nil); err != nil { return } SetBearerAuth(r, access.AppToken) SetCredentials(r, login) AddAccount(w, r) if err = ReadResponse(w, &profile); err != nil { return } guid = profile.GUID // acquire new token for attaching app notifications := []Notification{} if r, w, err = NewRequest("POST", "/account/apps", ¬ifications); err != nil { return } SetBasicAuth(r, login) AddAccountApp(w, r) if err = ReadResponse(w, &access); err != nil { return } token = access.AppToken // authorize claim if r, w, err = NewRequest("PUT", "/authorize?agent="+token, "1234abcd"); err != nil { return } Authorize(w, r) if err = ReadResponse(w, &msg); err != nil { return } signer, messageType, _, res := ReadDataMessage(&msg, &claim) if res != nil || signer != guid || messageType != APPMsgAuthenticate || claim.Token != "1234abcd" { err = errors.New("invalid authenticated claim") return } return } //NewRequest provides a lower level helper function for an endpoint test func NewRequest(rest string, path string, obj interface{}) (*http.Request, *httptest.ResponseRecorder, error) { w := httptest.NewRecorder() if obj != nil { body, err := json.Marshal(obj) if err != nil { return nil, nil, err } reader := strings.NewReader(string(body)) return httptest.NewRequest(rest, path, reader), w, nil } return httptest.NewRequest(rest, path, nil), w, nil } // Websocket test support type statusHandler struct{} func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Status(w, r) } //StatusConnection provides a test interface for the websocket updates func StatusConnection(token string, rev *Revision) (ws *websocket.Conn, err error) { var data []byte var dataType int // connect to websocket s := httptest.NewServer(&statusHandler{}) wsURL, _ := url.Parse(s.URL) wsURL.Scheme = "ws" if ws, _, err = websocket.DefaultDialer.Dial(wsURL.String(), nil); err != nil { return } // send authentication announce := Announce{AppToken: token} if data, err = json.Marshal(&announce); err != nil { return } ws.WriteMessage(websocket.TextMessage, data) // read revision response ws.SetReadDeadline(time.Now().Add(testReadDeadline * time.Second)) if dataType, data, err = ws.ReadMessage(); err != nil { return } if dataType != websocket.TextMessage { err = errors.New("invalid status data type") return } if err = json.Unmarshal(data, rev); err != nil { return } return }