diff --git a/db/config.go b/db/config.go index 29aeb2c1a..fe3d8e654 100644 --- a/db/config.go +++ b/db/config.go @@ -79,6 +79,7 @@ func InitDB() { db.AutoMigrate(&BountyRoles{}) db.AutoMigrate(&UserInvoiceData{}) db.AutoMigrate(&WorkspaceRepositories{}) + db.AutoMigrate(&WorkspaceCodeGraph{}) db.AutoMigrate(&WorkspaceFeatures{}) db.AutoMigrate(&FeaturePhase{}) db.AutoMigrate(&FeatureStory{}) diff --git a/db/test_config.go b/db/test_config.go index ad294609a..58901199b 100644 --- a/db/test_config.go +++ b/db/test_config.go @@ -63,6 +63,7 @@ func InitTestDB() { db.AutoMigrate(&Tickets{}) db.AutoMigrate(&ChatMessage{}) db.AutoMigrate(&Chat{}) + db.AutoMigrate(&WorkspaceCodeGraph{}) people := TestDB.GetAllPeople() for _, p := range people { diff --git a/handlers/workspaces.go b/handlers/workspaces.go index 95b484054..dedb3fc3c 100644 --- a/handlers/workspaces.go +++ b/handlers/workspaces.go @@ -1130,3 +1130,127 @@ func GetAllUserWorkspaces(pubkey string) []db.Workspace { return workspaces } + +func (oh *workspaceHandler) CreateOrEditWorkspaceCodeGraph(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("[workspaces] no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + codeGraph := db.WorkspaceCodeGraph{} + body, _ := io.ReadAll(r.Body) + r.Body.Close() + err := json.Unmarshal(body, &codeGraph) + + if err != nil { + fmt.Println("[workspaces]", err) + w.WriteHeader(http.StatusNotAcceptable) + return + } + + if len(codeGraph.Uuid) == 0 { + codeGraph.Uuid = xid.New().String() + codeGraph.CreatedBy = pubKeyFromAuth + } + codeGraph.UpdatedBy = pubKeyFromAuth + + err = db.Validate.Struct(codeGraph) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("Error: did not pass validation test : %s", err) + json.NewEncoder(w).Encode(msg) + return + } + + workspace := oh.db.GetWorkspaceByUuid(codeGraph.WorkspaceUuid) + if workspace.Uuid != codeGraph.WorkspaceUuid { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode("Workspace does not exist") + return + } + + p, err := oh.db.CreateOrEditCodeGraph(codeGraph) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(p) +} + +func (oh *workspaceHandler) GetWorkspaceCodeGraphByUUID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("[workspaces] no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + uuid := chi.URLParam(r, "uuid") + codeGraph, err := oh.db.GetCodeGraphByUUID(uuid) + if err != nil { + fmt.Println("[workspaces] code graph not found:", err) + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Code graph not found"}) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(codeGraph) +} + +func (oh *workspaceHandler) GetCodeGraphsByWorkspaceUuid(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("[workspaces] no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + workspace_uuid := chi.URLParam(r, "workspace_uuid") + codeGraphs, err := oh.db.GetCodeGraphsByWorkspaceUuid(workspace_uuid) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to get code graphs"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(codeGraphs) +} + +func (oh *workspaceHandler) DeleteWorkspaceCodeGraph(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("[workspaces] no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + workspace_uuid := chi.URLParam(r, "workspace_uuid") + uuid := chi.URLParam(r, "uuid") + + err := oh.db.DeleteCodeGraph(workspace_uuid, uuid) + if err != nil { + if err.Error() == "code graph not found" { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Code graph not found"}) + return + } + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to delete code graph"}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Code graph deleted successfully"}) +} diff --git a/handlers/workspaces_test.go b/handlers/workspaces_test.go index ae9a057a6..aca87698e 100644 --- a/handlers/workspaces_test.go +++ b/handlers/workspaces_test.go @@ -18,6 +18,7 @@ import ( "github.com/stakwork/sphinx-tribes/auth" "github.com/stakwork/sphinx-tribes/db" "github.com/stretchr/testify/assert" + "gorm.io/gorm" ) func TestUnitCreateOrEditWorkspace(t *testing.T) { @@ -1643,3 +1644,337 @@ func TestDeleteWorkspaceRepository(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) }) } + +func TestCreateOrEditWorkspaceCodeGraph(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + oHandler := NewWorkspaceHandler(db.TestDB) + + t.Run("should return error if a user is not authorized", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.CreateOrEditWorkspaceCodeGraph) + + bodyJson := []byte(`{"key": "value"}`) + ctx := context.WithValue(context.Background(), auth.ContextKey, "") + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/codegraph", bytes.NewReader(bodyJson)) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("should return error if body is not a valid json", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.CreateOrEditWorkspaceCodeGraph) + + invalidJson := []byte(`{"key": "value"`) + ctx := context.WithValue(context.Background(), auth.ContextKey, "pub-key") + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/codegraph", bytes.NewReader(invalidJson)) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusNotAcceptable, rr.Code) + }) + + t.Run("should return error if a Workspace UUID that does not exist is passed to the API body", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.CreateOrEditWorkspaceCodeGraph) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: uuid.New().String(), + OwnerPubKey: "workspace_owner_pubkey", + Github: "https://github.com/test", + Website: "https://www.test.com", + Description: "Test Description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + codeGraph := db.WorkspaceCodeGraph{ + Uuid: uuid.New().String(), + WorkspaceUuid: "wrongid", + Name: "testgraph", + Url: "https://github.com/test/graph", + } + + requestBody, _ := json.Marshal(codeGraph) + ctx := context.WithValue(context.Background(), auth.ContextKey, "pub-key") + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/codegraph", bytes.NewReader(requestBody)) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("user should be able to add a workspace code graph when the right conditions are met", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.CreateOrEditWorkspaceCodeGraph) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: uuid.New().String(), + OwnerPubKey: "workspace_owner_pubkey", + Github: "https://github.com/test", + Website: "https://www.test.com", + Description: "Test Description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + codeGraph := db.WorkspaceCodeGraph{ + Uuid: uuid.New().String(), + WorkspaceUuid: workspace.Uuid, + Name: "testgraph", + Url: "https://github.com/test/graph", + } + + requestBody, _ := json.Marshal(codeGraph) + ctx := context.WithValue(context.Background(), auth.ContextKey, "pub-key") + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/codegraph", bytes.NewReader(requestBody)) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + + var returnedCodeGraph db.WorkspaceCodeGraph + err = json.Unmarshal(rr.Body.Bytes(), &returnedCodeGraph) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, codeGraph.Name, returnedCodeGraph.Name) + assert.Equal(t, codeGraph.Url, returnedCodeGraph.Url) + }) +} + +func TestGetWorkspaceCodeGraphByUUID(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + oHandler := NewWorkspaceHandler(db.TestDB) + + person := db.Person{ + Uuid: uuid.New().String(), + OwnerAlias: "test-alias", + UniqueName: "test-unique-name", + OwnerPubKey: "test-pubkey", + PriceToMeet: 0, + Description: "test-description", + } + db.TestDB.CreateOrEditPerson(person) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: "test-workspace" + uuid.New().String(), + OwnerPubKey: person.OwnerPubKey, + Github: "https://github.com/test", + Website: "https://www.test.com", + Description: "test-description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + codeGraph := db.WorkspaceCodeGraph{ + Uuid: uuid.New().String(), + WorkspaceUuid: workspace.Uuid, + Name: "test-graph", + Url: "https://github.com/test/graph", + } + db.TestDB.CreateOrEditCodeGraph(codeGraph) + + t.Run("should return error if user is not authorized", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetWorkspaceCodeGraphByUUID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", codeGraph.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, "/codegraph/"+codeGraph.Uuid, nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("should return code graph if user is authorized", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetWorkspaceCodeGraphByUUID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", codeGraph.Uuid) + ctx := context.WithValue(context.Background(), auth.ContextKey, person.OwnerPubKey) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/codegraph/"+codeGraph.Uuid, nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + + var returnedCodeGraph db.WorkspaceCodeGraph + err = json.Unmarshal(rr.Body.Bytes(), &returnedCodeGraph) + assert.NoError(t, err) + assert.Equal(t, codeGraph.Name, returnedCodeGraph.Name) + assert.Equal(t, codeGraph.Url, returnedCodeGraph.Url) + }) +} + +func TestGetCodeGraphsByWorkspaceUuid(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + oHandler := NewWorkspaceHandler(db.TestDB) + + person := db.Person{ + Uuid: uuid.New().String(), + OwnerAlias: "test-alias", + UniqueName: "test-unique-name", + OwnerPubKey: "test-pubkey", + PriceToMeet: 0, + Description: "test-description", + } + db.TestDB.CreateOrEditPerson(person) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: "test-workspace" + uuid.New().String(), + OwnerPubKey: person.OwnerPubKey, + Github: "https://github.com/test", + Website: "https://www.test.com", + Description: "test-description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + codeGraph := db.WorkspaceCodeGraph{ + Uuid: uuid.New().String(), + WorkspaceUuid: workspace.Uuid, + Name: "test-graph", + Url: "https://github.com/test/graph", + } + db.TestDB.CreateOrEditCodeGraph(codeGraph) + + t.Run("should return error if user is not authorized", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetCodeGraphsByWorkspaceUuid) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("workspace_uuid", workspace.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, "/"+workspace.Uuid+"/codegraph", nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("should return workspace code graphs if user is authorized", func(t *testing.T) { + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetCodeGraphsByWorkspaceUuid) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("workspace_uuid", workspace.Uuid) + ctx := context.WithValue(context.Background(), auth.ContextKey, person.OwnerPubKey) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/"+workspace.Uuid+"/codegraph", nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + + var returnedCodeGraphs []db.WorkspaceCodeGraph + err = json.Unmarshal(rr.Body.Bytes(), &returnedCodeGraphs) + assert.NoError(t, err) + assert.Equal(t, 1, len(returnedCodeGraphs)) + assert.Equal(t, codeGraph.Name, returnedCodeGraphs[0].Name) + assert.Equal(t, codeGraph.Url, returnedCodeGraphs[0].Url) + }) +} + +func TestDeleteWorkspaceCodeGraph(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + oHandler := NewWorkspaceHandler(db.TestDB) + + person := db.Person{ + Uuid: "uuid", + OwnerAlias: "alias", + UniqueName: "unique_name", + OwnerPubKey: "pubkey", + PriceToMeet: 0, + Description: "description", + } + db.TestDB.CreateOrEditPerson(person) + + workspace := db.Workspace{ + Uuid: "workspace_uuid", + Name: "workspace_name", + OwnerPubKey: "person.OwnerPubkey", + Github: "github", + Website: "website", + Description: "description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + codeGraph := db.WorkspaceCodeGraph{ + Uuid: "graph_uuid", + WorkspaceUuid: workspace.Uuid, + Name: "graph_name", + Url: "graph_url", + } + db.TestDB.CreateOrEditCodeGraph(codeGraph) + + ctx := context.WithValue(context.Background(), auth.ContextKey, workspace.OwnerPubKey) + + t.Run("Should test that it throws a 401 error if a user is not authorized", func(t *testing.T) { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", codeGraph.Uuid) + rctx.URLParams.Add("workspace_uuid", workspace.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodDelete, "/"+workspace.Uuid+"/codegraph/"+codeGraph.Uuid, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.DeleteWorkspaceCodeGraph).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("Should test that the code graph is deleted after the Delete API request is successful", func(t *testing.T) { + existingGraph, err := db.TestDB.GetCodeGraphByUUID(codeGraph.Uuid) + if err != nil { + t.Fatal(err) + } + assert.NotEmpty(t, existingGraph) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", codeGraph.Uuid) + rctx.URLParams.Add("workspace_uuid", workspace.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodDelete, "/"+workspace.Uuid+"/codegraph/"+codeGraph.Uuid, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.DeleteWorkspaceCodeGraph).ServeHTTP(rr, req) + + _, err = db.TestDB.GetCodeGraphByUUID(codeGraph.Uuid) + assert.Error(t, err) + assert.Equal(t, gorm.ErrRecordNotFound.Error(), err.Error()) + assert.Equal(t, http.StatusOK, rr.Code) + }) +} diff --git a/routes/workspaces.go b/routes/workspaces.go index cb3ec3c7b..da2bb2491 100644 --- a/routes/workspaces.go +++ b/routes/workspaces.go @@ -55,6 +55,11 @@ func WorkspaceRoutes() chi.Router { r.Delete("/{workspace_uuid}/repository/{uuid}", workspaceHandlers.DeleteWorkspaceRepository) r.Get("/{workspace_uuid}/lastwithdrawal", workspaceHandlers.GetLastWithdrawal) + + r.Post("/codegraph", workspaceHandlers.CreateOrEditWorkspaceCodeGraph) + r.Get("/codegraph/{uuid}", workspaceHandlers.GetWorkspaceCodeGraphByUUID) + r.Get("/{workspace_uuid}/codegraph", workspaceHandlers.GetCodeGraphsByWorkspaceUuid) + r.Delete("/{workspace_uuid}/codegraph/{uuid}", workspaceHandlers.DeleteWorkspaceCodeGraph) }) return r }