diff --git a/cypress/e2e/06_phases.cy.js b/cypress/e2e/06_phases.cy.js new file mode 100644 index 000000000..d74b0bbf2 --- /dev/null +++ b/cypress/e2e/06_phases.cy.js @@ -0,0 +1,116 @@ +import { User, HostName, UserStories, Phases } from '../support/objects/objects'; + +describe('Create Phases for Feature', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'POST', + url: `${HostName}/features/phase`, + headers: { 'x-jwt': `${value}` }, + body: Phases[i] + }).its('body').then(body => { + expect(body).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(body).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(body).to.have.property('name').and.equal(Phases[i].name.trim()); + expect(body).to.have.property('priority').and.equal(Phases[i].priority); + }); + } + }) + }) +}) + +describe('Modify phases name', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'POST', + url: `${HostName}/features/phase`, + headers: { 'x-jwt': `${value}` }, + body: { + uuid: Phases[i].uuid, + name: Phases[i].name + "_addtext" + } + }).its('body').then(body => { + expect(body).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(body).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(body).to.have.property('name').and.equal(Phases[i].name.trim() + "_addtext"); + expect(body).to.have.property('priority').and.equal(Phases[i].priority); + }); + } + }) + }) +}) + +describe('Get phases for feature', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + for(let i = 0; i <= 2; i++) { + expect(resp.body[i]).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(resp.body[i]).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(resp.body[i]).to.have.property('name').and.equal(Phases[i].name.trim() + "_addtext"); + expect(resp.body[i]).to.have.property('priority').and.equal(Phases[i].priority); + } + }) + }) + }) +}) + +describe('Get phase by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[i].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + expect(resp.body[i]).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(resp.body[i]).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(resp.body[i]).to.have.property('name').and.equal(Phases[i].name.trim() + "_addtext"); + expect(resp.body[i]).to.have.property('priority').and.equal(Phases[i].priority); + }) + } + }) + }) +}) + +describe('Delete phase by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'DELETE', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + }) + }) + }) +}) + +describe('Check delete by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(404); + }) + }) + }) +}) diff --git a/cypress/e2e/06_phases.cy.ts b/cypress/e2e/06_phases.cy.ts new file mode 100644 index 000000000..bcc7dbd32 --- /dev/null +++ b/cypress/e2e/06_phases.cy.ts @@ -0,0 +1,120 @@ +import { User, HostName, UserStories, Phases } from '../support/objects/objects'; + +describe('Create Phases for Feature', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'POST', + url: `${HostName}/features/phase`, + headers: { 'x-jwt': `${value}` }, + body: Phases[i] + }).its('body').then(body => { + expect(body).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(body).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(body).to.have.property('name').and.equal(Phases[i].name.trim()); + expect(body).to.have.property('priority').and.equal(Phases[i].priority); + }); + } + }) + }) +}) + +describe('Modify phases name', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'POST', + url: `${HostName}/features/phase`, + headers: { 'x-jwt': `${value}` }, + body: { + uuid: Phases[i].uuid, + name: Phases[i].name + "_addtext" + } + }).its('body').then(body => { + expect(body).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(body).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(body).to.have.property('name').and.equal(Phases[i].name.trim() + " _addtext"); + expect(body).to.have.property('priority').and.equal(Phases[i].priority); + }); + } + }) + }) +}) + +describe('Get phases for feature', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + + resp.body.forEach((phase, index) => { + // Directly use index to compare with the expected phase in the same order + const expectedPhase = Phases[index]; + expect(phase.uuid).to.equal(expectedPhase.uuid.trim()); + expect(phase.feature_uuid).to.equal(expectedPhase.feature_uuid.trim()); + expect(phase.name).to.equal(expectedPhase.name.trim() + " _addtext"); + expect(phase.priority).to.equal(expectedPhase.priority); + }); + }) + }) + }) +}) + +describe('Get phase by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 2; i++) { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[i].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + expect(resp.body).to.have.property('uuid').and.equal(Phases[i].uuid.trim()); + expect(resp.body).to.have.property('feature_uuid').and.equal(Phases[i].feature_uuid.trim()); + expect(resp.body).to.have.property('name').and.equal(Phases[i].name.trim() + " _addtext"); + expect(resp.body).to.have.property('priority').and.equal(Phases[i].priority); + }) + } + }) + }) +}) + +describe('Delete phase by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'DELETE', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + }) + }) + }) +}) + +describe('Check delete by uuid', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'GET', + url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, + headers: { 'x-jwt': `${ value }` }, + body: {}, + failOnStatusCode: false + }).then((resp) => { + expect(resp.status).to.eq(404); + }) + }) + }) +}) diff --git a/cypress/support/objects/objects.ts b/cypress/support/objects/objects.ts index 9c1908ca8..9e4910605 100644 --- a/cypress/support/objects/objects.ts +++ b/cypress/support/objects/objects.ts @@ -138,7 +138,7 @@ export const UserStories = [ ]; export const Phases = [ - { uuid: 'com1msgn1e4a0ts5kls0', feature_uuid: 'com1kson1e49th88dbg0', name: ' MVP ' }, - { uuid: 'com1mvgn1e4a1879uiv0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 2 ' }, - { uuid: 'com1n2gn1e4a1i8p60p0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 3 ' }, + { uuid: 'com1msgn1e4a0ts5kls0', feature_uuid: 'com1kson1e49th88dbg0', name: ' MVP ', priority: 0 }, + { uuid: 'com1mvgn1e4a1879uiv0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 2 ', priority: 1 }, + { uuid: 'com1n2gn1e4a1i8p60p0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 3 ', priority: 2 }, ]; \ No newline at end of file diff --git a/db/config.go b/db/config.go index 3ffc2c3a1..b7ab4fa20 100644 --- a/db/config.go +++ b/db/config.go @@ -69,6 +69,7 @@ func InitDB() { db.AutoMigrate(&UserInvoiceData{}) db.AutoMigrate(&WorkspaceRepositories{}) db.AutoMigrate(&WorkspaceFeatures{}) + db.AutoMigrate(&FeaturePhase{}) DB.MigrateTablesWithOrgUuid() DB.MigrateOrganizationToWorkspace() diff --git a/db/features.go b/db/features.go index a4f0aad45..1372674f1 100644 --- a/db/features.go +++ b/db/features.go @@ -1,6 +1,7 @@ package db import ( + "errors" "fmt" "net/http" "strings" @@ -68,3 +69,49 @@ func (db database) CreateOrEditFeature(m WorkspaceFeatures) (WorkspaceFeatures, return m, nil } + +func (db database) CreateOrEditFeaturePhase(phase FeaturePhase) (FeaturePhase, error) { + phase.Name = strings.TrimSpace(phase.Name) + + now := time.Now() + phase.Updated = &now + + existingPhase := FeaturePhase{} + result := db.db.Model(&FeaturePhase{}).Where("uuid = ?", phase.Uuid).First(&existingPhase) + + if result.RowsAffected == 0 { + + phase.Created = &now + db.db.Create(&phase) + } else { + + db.db.Model(&FeaturePhase{}).Where("uuid = ?", phase.Uuid).Updates(phase) + } + + db.db.Model(&FeaturePhase{}).Where("uuid = ?", phase.Uuid).Find(&phase) + + return phase, nil +} + +func (db database) GetPhasesByFeatureUuid(featureUuid string) []FeaturePhase { + phases := []FeaturePhase{} + db.db.Model(&FeaturePhase{}).Where("feature_uuid = ?", featureUuid).Order("Created ASC").Find(&phases) + return phases +} + +func (db database) GetFeaturePhaseByUuid(featureUuid, phaseUuid string) (FeaturePhase, error) { + phase := FeaturePhase{} + result := db.db.Model(&FeaturePhase{}).Where("feature_uuid = ? AND uuid = ?", featureUuid, phaseUuid).First(&phase) + if result.RowsAffected == 0 { + return phase, errors.New("no phase found") + } + return phase, nil +} + +func (db database) DeleteFeaturePhase(featureUuid, phaseUuid string) error { + result := db.db.Where("feature_uuid = ? AND uuid = ?", featureUuid, phaseUuid).Delete(&FeaturePhase{}) + if result.RowsAffected == 0 { + return errors.New("no phase found to delete") + } + return nil +} diff --git a/db/interface.go b/db/interface.go index 0124592ad..1643e352c 100644 --- a/db/interface.go +++ b/db/interface.go @@ -146,4 +146,8 @@ type Database interface { GetFeaturesByWorkspaceUuid(uuid string, r *http.Request) []WorkspaceFeatures GetWorkspaceFeaturesCount(uuid string) int64 GetFeatureByUuid(uuid string) WorkspaceFeatures + CreateOrEditFeaturePhase(phase FeaturePhase) (FeaturePhase, error) + GetPhasesByFeatureUuid(featureUuid string) []FeaturePhase + GetFeaturePhaseByUuid(featureUuid, phaseUuid string) (FeaturePhase, error) + DeleteFeaturePhase(featureUuid, phaseUuid string) error } diff --git a/db/structs.go b/db/structs.go index 06c6100de..020b5e4b8 100644 --- a/db/structs.go +++ b/db/structs.go @@ -577,6 +577,17 @@ type WorkspaceFeatures struct { UpdatedBy string `json:"updated_by"` } +type FeaturePhase struct { + Uuid string `json:"uuid" gorm:"primary_key"` + FeatureUuid string `json:"feature_uuid"` + Name string `json:"name"` + Priority int `json:"priority"` + Created *time.Time `json:"created"` + Updated *time.Time `json:"updated"` + CreatedBy string `json:"created_by"` + UpdatedBy string `json:"updated_by"` +} + type BountyRoles struct { Name string `json:"name"` } diff --git a/handlers/features.go b/handlers/features.go index ea634cfb0..1e2096623 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -3,13 +3,12 @@ package handlers import ( "encoding/json" "fmt" - "io" - "net/http" - "github.com/go-chi/chi" "github.com/rs/xid" "github.com/stakwork/sphinx-tribes/auth" "github.com/stakwork/sphinx-tribes/db" + "io" + "net/http" ) type featureHandler struct { @@ -116,3 +115,81 @@ func (oh *featureHandler) GetFeatureByUuid(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(workspaceFeature) } + +func (oh *featureHandler) CreateOrEditFeaturePhase(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + newPhase := db.FeaturePhase{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&newPhase) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Error decoding request body: %v", err) + return + } + + if newPhase.Uuid == "" { + newPhase.Uuid = xid.New().String() + } + + existingPhase, _ := oh.db.GetFeaturePhaseByUuid(newPhase.FeatureUuid, newPhase.Uuid) + + if existingPhase.CreatedBy == "" { + newPhase.CreatedBy = pubKeyFromAuth + } + + newPhase.UpdatedBy = pubKeyFromAuth + + phase, err := oh.db.CreateOrEditFeaturePhase(newPhase) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Error creating feature phase: %v", err) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(phase) +} + +func (oh *featureHandler) GetFeaturePhases(w http.ResponseWriter, r *http.Request) { + featureUuid := chi.URLParam(r, "feature_uuid") + phases := oh.db.GetPhasesByFeatureUuid(featureUuid) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(phases) +} + +func (oh *featureHandler) GetFeaturePhaseByUUID(w http.ResponseWriter, r *http.Request) { + featureUuid := chi.URLParam(r, "feature_uuid") + phaseUuid := chi.URLParam(r, "phase_uuid") + + phase, err := oh.db.GetFeaturePhaseByUuid(featureUuid, phaseUuid) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(phase) +} + +func (oh *featureHandler) DeleteFeaturePhase(w http.ResponseWriter, r *http.Request) { + featureUuid := chi.URLParam(r, "feature_uuid") + phaseUuid := chi.URLParam(r, "phase_uuid") + + err := oh.db.DeleteFeaturePhase(featureUuid, phaseUuid) + if err != nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Phase deleted successfully"}) +} diff --git a/mocks/Database.go b/mocks/Database.go index 6f68771bc..92bbc00de 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -6811,3 +6811,75 @@ func NewDatabase(t interface { return mock } + +// CreateOrEditFeaturePhase provides a mock function with given fields: phase +func (_m *Database) CreateOrEditFeaturePhase(phase db.FeaturePhase) (db.FeaturePhase, error) { + ret := _m.Called(phase) + + var r0 db.FeaturePhase + var r1 error + if rf, ok := ret.Get(0).(func(db.FeaturePhase) db.FeaturePhase); ok { + r0 = rf(phase) + } else { + r0 = ret.Get(0).(db.FeaturePhase) + } + + if rf, ok := ret.Get(1).(func(db.FeaturePhase) error); ok { + r1 = rf(phase) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPhasesByFeatureUuid provides a mock function with given fields: featureUuid +func (_m *Database) GetPhasesByFeatureUuid(featureUuid string) []db.FeaturePhase { + ret := _m.Called(featureUuid) + + var r0 []db.FeaturePhase + if rf, ok := ret.Get(0).(func(string) []db.FeaturePhase); ok { + r0 = rf(featureUuid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.FeaturePhase) + } + } + + return r0 +} + +// GetFeaturePhaseByUuid provides a mock function with given fields: featureUuid, phaseUuid +func (_m *Database) GetFeaturePhaseByUuid(featureUuid, phaseUuid string) (db.FeaturePhase, error) { + ret := _m.Called(featureUuid, phaseUuid) + + var r0 db.FeaturePhase + var r1 error + if rf, ok := ret.Get(0).(func(string, string) db.FeaturePhase); ok { + r0 = rf(featureUuid, phaseUuid) + } else { + r0 = ret.Get(0).(db.FeaturePhase) + } + + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(featureUuid, phaseUuid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteFeaturePhase provides a mock function with given fields: featureUuid, phaseUuid +func (_m *Database) DeleteFeaturePhase(featureUuid string, phaseUuid string) error { + ret := _m.Called(featureUuid, phaseUuid) + + var r1 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r1 = rf(featureUuid, phaseUuid) + } else { + r1 = ret.Error(0) + } + + return r1 +} diff --git a/routes/features.go b/routes/features.go index a6189793a..405b7ce60 100644 --- a/routes/features.go +++ b/routes/features.go @@ -9,7 +9,7 @@ import ( func FeatureRoutes() chi.Router { r := chi.NewRouter() - featureHandlers := handlers.NewFeatureHandler(db.DB) + featureHandlers := handlers.NewFeatureHandler(&db.DB) r.Group(func(r chi.Router) { r.Use(auth.PubKeyContext) @@ -17,6 +17,12 @@ func FeatureRoutes() chi.Router { r.Get("/forworkspace/{uuid}", featureHandlers.GetFeaturesByWorkspaceUuid) r.Get("/{uuid}", featureHandlers.GetFeatureByUuid) r.Get("/workspace/count/{uuid}", featureHandlers.GetWorkspaceFeaturesCount) + + r.Post("/phase", featureHandlers.CreateOrEditFeaturePhase) + r.Get("/{feature_uuid}/phase", featureHandlers.GetFeaturePhases) + r.Get("/{feature_uuid}/phase/{phase_uuid}", featureHandlers.GetFeaturePhaseByUUID) + r.Delete("/{feature_uuid}/phase/{phase_uuid}", featureHandlers.DeleteFeaturePhase) + }) return r }