package main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "local/dndex/server/auth" "local/dndex/storage/entity" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/google/uuid" ) func Test(t *testing.T) { GitCommit = uuid.New().String() d, err := ioutil.TempDir(os.TempDir(), "test.dndex") if err != nil { t.Fatal(err) } s := httptest.NewServer(http.HandlerFunc(nil)) s.Close() p := strings.Split(s.URL, ":")[2] os.Args = strings.Split(fmt.Sprintf(`dndex -auth=true -database db -delay 5ms -driver map -fileprefix /files -fileroot %s -p %v -rps 50 -sys-rps 40`, d, p), " ") go main() for { resp, err := http.Get(s.URL) if err == nil { resp.Body.Close() break } time.Sleep(time.Millisecond * 100) } token := register(t, s.URL) t.Logf("token: %q", token) createDelete(t, s.URL, token) createUpdate(t, s.URL, token) callVersion(t, s.URL) muckedToken(t, s.URL, token) createDeleteSub(t, s.URL, token) createUpdateSub(t, s.URL, token) filesCRUD(t, s.URL, token) } func createDeleteSub(t *testing.T, uri, token string) { t.Run("create, delete sub", func(t *testing.T) { id := callCreate(t, uri, token, "") was := callGet(t, uri, token, id, http.StatusOK) callDelete(t, uri, token, id+"/title") one := callGet(t, uri, token, id, http.StatusOK) if one.Title != "" { t.Fatal(one.Title) } was.Title = "" was.Modified = 0 one.Modified = 0 if fmt.Sprint(one) != fmt.Sprint(was) { t.Fatalf("partial del failed: want \n%+v, got \n%+v", was, one) } }) } func createUpdateSub(t *testing.T, uri, token string) { t.Run("create, update sub", func(t *testing.T) { id := callCreate(t, uri, token, "") was := callGet(t, uri, token, id, http.StatusOK) resp := call(t, token, http.MethodPut, uri+"/entities/"+id+"/title", `"newtitle"`) if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } one := callGet(t, uri, token, id, http.StatusOK) if one.Title != "newtitle" { t.Fatal(one.Title) } was.Title = "newtitle" was.Modified = 0 one.Modified = 0 if fmt.Sprint(one) != fmt.Sprint(was) { t.Fatalf("partial updated failed: want \n%+v, got \n%+v", was, one) } }) } func filesCRUD(t *testing.T, uri, token string) { t.Run("files CRUD", func(t *testing.T) { t.Log("not impl") }) } func register(t *testing.T, uri string) string { callList(t, uri, "", http.StatusUnauthorized) ns, pwd := callRegister(t, uri) token := callLogin(t, uri, ns, pwd) callList(t, uri, token, http.StatusOK) return token } func muckedToken(t *testing.T, uri, obf string) { var token auth.Token if err := token.Deobfuscate(obf); err != nil { t.Fatal(err) } if token.Namespace == "" || token.ID == "" { t.Fatalf("ns: %q, id: %q", token.Namespace, token.ID) } namespace := token.Namespace id := token.ID for _, token := range []auth.Token{ auth.Token{ Namespace: namespace, ID: uuid.New().String(), }, auth.Token{ Namespace: uuid.New().String(), ID: id, }, } { callList(t, uri, token.String(), http.StatusUnauthorized) } } func createUpdate(t *testing.T, uri, token string) { t.Run("create, update", func(t *testing.T) { id := callCreate(t, uri, token, "") callUpdate(t, uri, token, id) }) } func createDelete(t *testing.T, uri, token string) { t.Run("create, delete", func(t *testing.T) { id := callCreate(t, uri, token, "") callDelete(t, uri, token, id) one := callGet(t, uri, token, id, http.StatusNotFound) if one.ID == id { t.Fatal(one) } if fmt.Sprint(one) != fmt.Sprint(entity.One{}) { t.Fatal(one) } }) } func callVersion(t *testing.T, uri string) { t.Run("call /version", func(t *testing.T) { resp := call(t, "", http.MethodGet, uri+"/version", "") if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } var response struct { Version string `json:"version"` } b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } if err := json.Unmarshal(b, &response); err != nil { t.Fatal(err) } if response.Version != GitCommit { t.Fatalf("%s: %v", b, response) } }) } func callGet(t *testing.T, uri, token, id string, status int) entity.One { resp := call(t, token, http.MethodGet, uri+"/entities/"+id, "") if resp.StatusCode != status { t.Fatal(resp.StatusCode) } if status != http.StatusOK { return entity.One{} } b, _ := ioutil.ReadAll(resp.Body) var response map[string]entity.One if err := json.Unmarshal(b, &response); err != nil { t.Fatal(err) } for _, one := range response { if fmt.Sprint(one) == fmt.Sprint(entity.One{}) { t.Fatal(id, status, one, string(b)) } return one } t.Fatal("no items in response") return entity.One{} } func callDelete(t *testing.T, uri, token, id string) { t.Run("call delete /entities/X", func(t *testing.T) { resp := call(t, token, http.MethodDelete, uri+"/entities/"+id, "") if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } }) } func callUpdate(t *testing.T, uri, token, id string) { t.Run("call update /entities/X", func(t *testing.T) { for _, method := range []string{http.MethodPut, http.MethodPatch} { m := map[string]interface{}{ "name": "name-" + uuid.New().String()[:5], "type": "type-" + uuid.New().String()[:5], "title": "titl-" + uuid.New().String()[:5], "text": "text-" + uuid.New().String()[:5], "connections": map[string]map[string]string{ callCreate(t, uri, token, ""): map[string]string{"relationship": "callUpdate peer"}, }, "attachments": map[string]map[string]string{ "myfile": map[string]string{"location": "/files/my_file_location.txt"}, }, } b, err := json.Marshal(m) if err != nil { t.Fatal(err) } resp := call(t, token, method, uri+"/entities/"+id, string(b)) if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } var response map[string]map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { t.Fatal(err) } found := false for k := range response { for k2 := range m { if k2 == entity.Connections { if v, ok := response[k][k2]; !ok { t.Fatal("missing connection", k2) } else if m2, ok := v.(map[string]interface{}); !ok { t.Fatalf("wrong connection type %T", v) } else { wantConnections := m[k2].(map[string]map[string]string) for k3 := range wantConnections { wantRelationship := wantConnections[k3][entity.Relationship] gotRelationship := m2[k3].(map[string]interface{})[entity.Relationship] if wantRelationship != gotRelationship { t.Fatal(gotRelationship, wantRelationship) } b, _ := json.Marshal(m2[k3]) var gotOne entity.One json.Unmarshal(b, &gotOne) if k3 != gotOne.ID { t.Fatal(gotOne.ID) } if "" == gotOne.Text { t.Fatal(gotOne) } } } } else { if fmt.Sprint(m[k2]) != fmt.Sprint(response[k][k2]) { t.Fatalf("@%q.%q, want %q, got %q", k, k2, fmt.Sprint(m[k2]), fmt.Sprint(response[k][k2])) } } } found = true } if !found { t.Fatal(found) } } }) } func callCreate(t *testing.T, uri, token, peerID string) string { connections := map[string]map[string]string{} if peerID != "" { connections[peerID] = map[string]string{"relationship": "call Create peer"} } m := map[string]interface{}{ "name": "name-" + uuid.New().String()[:5], "type": "type-" + uuid.New().String()[:5], "title": "titl-" + uuid.New().String()[:5], "text": "text-" + uuid.New().String()[:5], "connections": connections, "attachments": map[string]map[string]string{ "myfile": map[string]string{"location": "/files/my_file_location.txt"}, }, } b, err := json.Marshal(m) if err != nil { t.Fatal(err) } resp := call(t, token, http.MethodPost, uri+"/entities", string(b)) if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } var response map[string]map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { t.Fatal(err) } for k := range response { for k2 := range m { if fmt.Sprint(m[k2]) != fmt.Sprint(response[k][k2]) { t.Fatal(k2, response[k][k2]) } } return k } t.Fatal(response) panic("how?") } func callList(t *testing.T, uri, token string, status int) { t.Run("call get /entities", func(t *testing.T) { resp := call(t, token, http.MethodGet, uri+"/entities", "") b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } if resp.StatusCode != status { t.Fatalf("%v: %s", resp.StatusCode, b) } }) } func callRegister(t *testing.T, uri string) (string, string) { ns := uuid.New().String() pwd := uuid.New().String() resp, err := http.Post(uri+"/users/register", "application/x-www-form-urlencoded", strings.NewReader(fmt.Sprintf("%s=%s&%s=%s", auth.UserKey, ns, auth.AuthKey, pwd))) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } return ns, pwd } func callLogin(t *testing.T, uri, ns, pwd string) string { resp, err := http.Post(uri+"/users/login", "application/x-www-form-urlencoded", strings.NewReader(fmt.Sprintf("%s=%s", auth.UserKey, ns))) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatal(resp.StatusCode) } b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } var response struct { OK struct { EncodedToken string `json:"token"` Salt string `json:"salt"` } `json:"ok"` } if err := json.Unmarshal(b, &response); err != nil { t.Fatalf("%v: %s", err, b) } salt := response.OK.Salt encodedToken := response.OK.EncodedToken if len(salt) == 0 { t.Fatal("salt empty") } if len(encodedToken) == 0 { t.Fatal("token empty") } token := auth.Token{} if err := token.Decode(salt+pwd, encodedToken); err != nil { t.Fatal(err) } return token.String() } func call(t *testing.T, token, method, uri, body string) *http.Response { r, err := http.NewRequest(method, uri, strings.NewReader(body)) if err != nil { t.Fatal(err) } r.AddCookie(&http.Cookie{Name: auth.AuthKey, Value: token}) resp, err := http.DefaultClient.Do(r) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } resp.Body = struct { io.Reader io.Closer }{ Closer: resp.Body, Reader: bytes.NewReader(b), } return resp }