From 0a3f966946456971326b39145f25219823bf1d32 Mon Sep 17 00:00:00 2001 From: l Date: Tue, 30 Apr 2024 08:53:49 -0400 Subject: [PATCH 01/12] Added cypress tests and objects --- cypress/e2e/02_repositories.cy.ts | 38 ++++++++++++++++++ cypress/support/objects/objects.ts | 64 +++++++++++++++++------------- 2 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 cypress/e2e/02_repositories.cy.ts diff --git a/cypress/e2e/02_repositories.cy.ts b/cypress/e2e/02_repositories.cy.ts new file mode 100644 index 000000000..f3484ba1b --- /dev/null +++ b/cypress/e2e/02_repositories.cy.ts @@ -0,0 +1,38 @@ +import { User, HostName, Workspaces, Repositories } from '../support/objects/objects'; + + +describe('Create Repositories for Workspace', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + for(let i = 0; i <= 1; i++) { + cy.request({ + method: 'POST', + url: `${HostName}/workspaces/repositories`, + headers: { 'x-jwt': `${value}` }, + body: Repositories[i] + }).its('body').should('have.property', 'name', Repositories[i].name.trim()) + .its('body').should('have.property', 'url', Repositories[i].url.trim()); + } + }) + }) +}) + + +describe('Check Repositories Values', () => { + it('passes', () => { + cy.upsertlogin(User).then(value => { + cy.request({ + method: 'GET', + url: `${HostName}/workspaces/repositories/` + Repositories[0].workspace_uuid, + headers: { 'x-jwt': `${ value }` }, + body: {} + }).then((resp) => { + expect(resp.status).to.eq(200) + expect(resp.body[0]).to.have.property('name', Repositories[0].name.trim()) + expect(resp.body[0]).to.have.property('url', Repositories[0].url.trim()) + expect(resp.body[1]).to.have.property('name', Repositories[1].name.trim()) + expect(resp.body[1]).to.have.property('url', Repositories[1].url.trim()) + }) + }) + }) +}) diff --git a/cypress/support/objects/objects.ts b/cypress/support/objects/objects.ts index b62af74d7..bfae6de06 100644 --- a/cypress/support/objects/objects.ts +++ b/cypress/support/objects/objects.ts @@ -57,34 +57,42 @@ export const Workspaces = [ export const Repositories = [ { - name: 'frontend', - url: 'https://github.com/stakwork/sphinx-tribes-frontend' + uuid: 'com1t3gn1e4a4qu3tnlg', + workspace_uuid: 'cohob00n1e4808utqel0', + name: ' frontend ', + url: ' https://github.com/stakwork/sphinx-tribes-frontend ' }, { - name: 'backend', - url: 'https://github.com/stakwork/sphinx-tribes' + uuid: 'com1t3gn1e4a4qu3tnlg', + workspace_uuid: 'cohob00n1e4808utqel0', + name: ' backend ', + url: ' https://github.com/stakwork/sphinx-tribes ' } ]; export const Features = [ { + uuid: 'com1kson1e49th88dbg0', + workspace_uuid: 'cohob00n1e4808utqel0', name: 'Hive Process', priority: 1, - brief: 'To follow a set of best practices in product development.
' + + brief: ' To follow a set of best practices in product development.
' + 'Dividing complex features into small
steps makes it easier to ' + 'track and the timing more certain.
A guided process would help ' + 'a PM new to the hive process get the best results with the least mental ' + 'load.
This feature is for a not se technical Product Manager.
' + - 'The hive process lets you get features out to production faster and with less risk.', - requirements: 'Modify workspaces endpoint to accomodate new fields.
' + - 'Create end points for features, user stories and phases', - architecture: 'Describe the architecture of the feature with the following sections:' + + 'The hive process lets you get features out to production faster and with less risk. ', + requirements: ' Modify workspaces endpoint to accomodate new fields.
' + + 'Create end points for features, user stories and phases ', + architecture: ' Describe the architecture of the feature with the following sections:' + '

Wireframes

Visual Schematics

Object Definition

' + 'DB Schema Changes

UX

CI/CD

Changes

Endpoints

' + - 'Front

', + 'Front

', }, { - name: 'AI Assited text fields', + uuid: 'com1l5on1e49tucv350g', + workspace_uuid: 'cohob00n1e4808utqel0', + name: ' AI Assited text fields ', priority: 2, brief: 'An important struggle of a technical product manager is to find ' + 'the right words to describe a business goal. The definition of ' + @@ -93,18 +101,20 @@ export const Features = [ 'We are going to leverage AI to help the PM write better definitions.
' + 'The fields that would benefit form AI assistance are: mission, tactics, ' + 'feature brief and feature user stories', - requirements: 'Create a new page for a conversation format between the PM and the LLM
' + + requirements: ' Create a new page for a conversation format between the PM and the LLM
' + 'Rely as much as possible on stakwork workflows
' + - 'Have history of previous definitions', - architecture: 'Describe the architecture of the feature with the following sections:' + + 'Have history of previous definitions ', + architecture: ' Describe the architecture of the feature with the following sections:' + '

Wireframes

Visual Schematics

Object Definition

' + 'DB Schema Changes

UX

CI/CD

Changes

Endpoints

' + - 'Front

', + 'Front

', }, { - name: 'AI Assited relation between text fields', + uuid: 'com1l5on1e49tucv350g', + workspace_uuid: 'cohob00n1e4808utqel0', + name: ' AI Assited relation between text fields ', priority: 2, - brief: 'A product and feature\'s various definition fields: mission, tactics, ' + + brief: ' A product and feature\'s various definition fields: mission, tactics, ' + 'feature brief, user stories, requirements and architecture should have some ' + 'relation between each other.
' + 'One way to do that is to leverage an LLM ' + 'to discern the parts of the defintion that have a connection to other definitions.
' + @@ -114,21 +124,21 @@ export const Features = [ architecture: 'Describe the architecture of the feature with the following sections:' + '

Wireframes

Visual Schematics

Object Definition

' + 'DB Schema Changes

UX

CI/CD

Changes

Endpoints

' + - 'Front

', + 'Front

', }, ]; export const UserStories = [ - { id: 'f4c4c4b4-7a90-4a3a-b3e2-151d0feca9bf', description: ' As a {PM} I want to {make providers \"hive ready\"}, so I can {leverage the hive process ' }, - { id: '78f4b326-1841-449b-809a-a0947622db3e', description: ' As a {PM} I want to {CRUD Features}, so I can {use the system to manage my features} ' }, - { id: '5d353d23-3d27-4aa8-a9f7-04dcd5f4843c', description: ' As a {PM} I want to {follow best practices}, so I can {make more valuable features} ' }, - { id: '1a4e00f4-0e58-4e08-a1df-b623bc10f08d', description: ' As a {PM} I want to {save the architecture of the feature}, so I can {share it with people} ' }, - { id: 'eb6e4138-37e5-465d-934e-18e335abaa47', description: ' As a {PM} I want to {create phases}, so I can {divide the work in several deliverable stages} ' }, - { id: '35a5d8dd-240d-4ff0-a699-aa2fa2cfa32c', description: ' As a {PM} I want to {assign bounties to features}, so I can {group bounties together} ' }, + { uuid: 'com1lh0n1e49ug76noig', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {make providers \"hive ready\"}, so I can {leverage the hive process ' }, + { uuid: 'com1lk8n1e49uqfe3l40', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {CRUD Features}, so I can {use the system to manage my features} ' }, + { uuid: 'com1ln8n1e49v4159gug', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {follow best practices}, so I can {make more valuable features} ' }, + { uuid: 'com1lqgn1e49vevhs9k0', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {save the architecture of the feature}, so I can {share it with people} ' }, + { uuid: 'com1lt8n1e49voquoq90', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {create phases}, so I can {divide the work in several deliverable stages} ' }, + { uuid: 'com1m08n1e4a02r6j0pg', feature_uuid: 'com1kson1e49th88dbg0', description: ' As a {PM} I want to {assign bounties to features}, so I can {group bounties together} ' }, ]; export const Phases = [ - { id: 'a96e3bff-e5c8-429e-bd65-911d619761aa', name: ' MVP ' }, - { id: '6de147ab-695c-45b1-81e7-2d1a5ba482ab', name: ' MVP ' }, - { id: '28541c4a-41de-447e-86d8-293583d1abc2', name: ' MVP ' }, + { uuid: 'com1msgn1e4a0ts5kls0', feature_uuid: 'com1kson1e49th88dbg0', name: ' MVP ' }, + { uuid: 'com1mvgn1e4a1879uiv0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 2 ' }, + { uuid: 'com1n2gn1e4a1i8p60p0', feature_uuid: 'com1kson1e49th88dbg0', name: ' Phase 3 ' }, ]; \ No newline at end of file From 97b8b4b7a51ec900f8ab3a5b91b32072821d6ac0 Mon Sep 17 00:00:00 2001 From: Mirza Hanan Date: Fri, 3 May 2024 23:28:09 +0500 Subject: [PATCH 02/12] Add Endpoints for Workspace Repositories --- cypress/e2e/02_repositories.cy.ts | 6 +- cypress/support/objects/objects.ts | 2 +- db/config.go | 1 + db/interface.go | 2 + db/structs.go | 12 ++++ db/workspaces.go | 24 +++++++ handlers/workspaces.go | 57 ++++++++++++++++ mocks/Database.go | 104 +++++++++++++++++++++++++++++ routes/workspaces.go | 3 + 9 files changed, 208 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/02_repositories.cy.ts b/cypress/e2e/02_repositories.cy.ts index f3484ba1b..dba204270 100644 --- a/cypress/e2e/02_repositories.cy.ts +++ b/cypress/e2e/02_repositories.cy.ts @@ -10,8 +10,10 @@ describe('Create Repositories for Workspace', () => { url: `${HostName}/workspaces/repositories`, headers: { 'x-jwt': `${value}` }, body: Repositories[i] - }).its('body').should('have.property', 'name', Repositories[i].name.trim()) - .its('body').should('have.property', 'url', Repositories[i].url.trim()); + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Repositories[i].name.trim()); + expect(body).to.have.property('url').and.equal(Repositories[i].url.trim()); + }); } }) }) diff --git a/cypress/support/objects/objects.ts b/cypress/support/objects/objects.ts index bfae6de06..e1a2f7328 100644 --- a/cypress/support/objects/objects.ts +++ b/cypress/support/objects/objects.ts @@ -63,7 +63,7 @@ export const Repositories = [ url: ' https://github.com/stakwork/sphinx-tribes-frontend ' }, { - uuid: 'com1t3gn1e4a4qu3tnlg', + uuid: 'com1t3gn1e4a4qu3thss', workspace_uuid: 'cohob00n1e4808utqel0', name: ' backend ', url: ' https://github.com/stakwork/sphinx-tribes ' diff --git a/db/config.go b/db/config.go index 4b50a4502..547ca97a1 100644 --- a/db/config.go +++ b/db/config.go @@ -67,6 +67,7 @@ func InitDB() { db.AutoMigrate(&ConnectionCodes{}) db.AutoMigrate(&BountyRoles{}) db.AutoMigrate(&UserInvoiceData{}) + db.AutoMigrate(&WorkspaceRepositories{}) DB.MigrateTablesWithOrgUuid() DB.MigrateOrganizationToWorkspace() diff --git a/db/interface.go b/db/interface.go index e3cc95163..691c737dd 100644 --- a/db/interface.go +++ b/db/interface.go @@ -139,4 +139,6 @@ type Database interface { PersonUniqueNameFromName(name string) (string, error) ProcessAlerts(p Person) UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool + CreateWorkspaceRepository(m WorkspaceRepositories) (WorkspaceRepositories, error) + GetWorkspaceRepositorByWorkspaceUuid(uuid string) []WorkspaceRepositories } diff --git a/db/structs.go b/db/structs.go index e6a25a7de..c4aba003e 100644 --- a/db/structs.go +++ b/db/structs.go @@ -547,6 +547,18 @@ type WorkspaceUsersData struct { Person } +type WorkspaceRepositories struct { + ID uint `json:"id"` + Uuid string `json:"uuid"` + WorkspaceUuid string `json:"workspace_uuid"` + Name string `json:"name"` + Url string `json:"url"` + 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/db/workspaces.go b/db/workspaces.go index d1e77b596..406be7d01 100644 --- a/db/workspaces.go +++ b/db/workspaces.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/stakwork/sphinx-tribes/utils" @@ -52,6 +53,29 @@ func (db database) CreateOrEditWorkspace(m Workspace) (Workspace, error) { return m, nil } +func (db database) CreateWorkspaceRepository(m WorkspaceRepositories) (WorkspaceRepositories, error) { + m.Name = strings.TrimSpace(m.Name) + m.Url = strings.TrimSpace(m.Url) + + now := time.Now() + m.Updated = &now + + if db.db.Model(&m).Where("uuid = ?", m.Uuid).Updates(&m).RowsAffected == 0 { + m.Created = &now + db.db.Create(&m) + } + + return m, nil +} + +func (db database) GetWorkspaceRepositorByWorkspaceUuid(uuid string) []WorkspaceRepositories { + ms := []WorkspaceRepositories{} + + db.db.Model(&WorkspaceRepositories{}).Where("workspace_uuid = ?", uuid).Order("Created").Find(&ms) + + return ms +} + func (db database) GetWorkspaceUsers(uuid string) ([]WorkspaceUsersData, error) { ms := []WorkspaceUsersData{} diff --git a/handlers/workspaces.go b/handlers/workspaces.go index 0b152c96d..5c8138d11 100644 --- a/handlers/workspaces.go +++ b/handlers/workspaces.go @@ -795,3 +795,60 @@ func (oh *workspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(p) } + +func (oh *workspaceHandler) CreateWorkspaceRepository(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 + } + + workspaceRepo := db.WorkspaceRepositories{} + body, _ := io.ReadAll(r.Body) + r.Body.Close() + err := json.Unmarshal(body, &workspaceRepo) + + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusNotAcceptable) + return + } + + workspaceRepo.CreatedBy = pubKeyFromAuth + + // Validate struct data + err = db.Validate.Struct(workspaceRepo) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("Error: did not pass validation test : %s", err) + json.NewEncoder(w).Encode(msg) + return + } + + p, err := oh.db.CreateWorkspaceRepository(workspaceRepo) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(p) +} + +func (oh *workspaceHandler) GetWorkspaceRepositorByWorkspaceUuid(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 + } + + uuid := chi.URLParam(r, "uuid") + workspaceFeatures := oh.db.GetWorkspaceRepositorByWorkspaceUuid(uuid) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(workspaceFeatures) +} diff --git a/mocks/Database.go b/mocks/Database.go index 784a2492a..aeff38189 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -1191,6 +1191,62 @@ func (_c *Database_CreateWorkspaceBudget_Call) RunAndReturn(run func(db.NewBount return _c } +// CreateWorkspaceRepository provides a mock function with given fields: m +func (_m *Database) CreateWorkspaceRepository(m db.WorkspaceRepositories) (db.WorkspaceRepositories, error) { + ret := _m.Called(m) + + if len(ret) == 0 { + panic("no return value specified for CreateWorkspaceRepository") + } + + var r0 db.WorkspaceRepositories + var r1 error + if rf, ok := ret.Get(0).(func(db.WorkspaceRepositories) (db.WorkspaceRepositories, error)); ok { + return rf(m) + } + if rf, ok := ret.Get(0).(func(db.WorkspaceRepositories) db.WorkspaceRepositories); ok { + r0 = rf(m) + } else { + r0 = ret.Get(0).(db.WorkspaceRepositories) + } + + if rf, ok := ret.Get(1).(func(db.WorkspaceRepositories) error); ok { + r1 = rf(m) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Database_CreateWorkspaceRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateWorkspaceRepository' +type Database_CreateWorkspaceRepository_Call struct { + *mock.Call +} + +// CreateWorkspaceRepository is a helper method to define mock.On call +// - m db.WorkspaceRepositories +func (_e *Database_Expecter) CreateWorkspaceRepository(m interface{}) *Database_CreateWorkspaceRepository_Call { + return &Database_CreateWorkspaceRepository_Call{Call: _e.mock.On("CreateWorkspaceRepository", m)} +} + +func (_c *Database_CreateWorkspaceRepository_Call) Run(run func(m db.WorkspaceRepositories)) *Database_CreateWorkspaceRepository_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(db.WorkspaceRepositories)) + }) + return _c +} + +func (_c *Database_CreateWorkspaceRepository_Call) Return(_a0 db.WorkspaceRepositories, _a1 error) *Database_CreateWorkspaceRepository_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Database_CreateWorkspaceRepository_Call) RunAndReturn(run func(db.WorkspaceRepositories) (db.WorkspaceRepositories, error)) *Database_CreateWorkspaceRepository_Call { + _c.Call.Return(run) + return _c +} + // CreateWorkspaceUser provides a mock function with given fields: orgUser func (_m *Database) CreateWorkspaceUser(orgUser db.WorkspaceUsers) db.WorkspaceUsers { ret := _m.Called(orgUser) @@ -4745,6 +4801,54 @@ func (_c *Database_GetWorkspaceInvoicesCount_Call) RunAndReturn(run func(string) return _c } +// GetWorkspaceRepositorByWorkspaceUuid provides a mock function with given fields: uuid +func (_m *Database) GetWorkspaceRepositorByWorkspaceUuid(uuid string) []db.WorkspaceRepositories { + ret := _m.Called(uuid) + + if len(ret) == 0 { + panic("no return value specified for GetWorkspaceRepositorByWorkspaceUuid") + } + + var r0 []db.WorkspaceRepositories + if rf, ok := ret.Get(0).(func(string) []db.WorkspaceRepositories); ok { + r0 = rf(uuid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.WorkspaceRepositories) + } + } + + return r0 +} + +// Database_GetWorkspaceRepositorByWorkspaceUuid_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceRepositorByWorkspaceUuid' +type Database_GetWorkspaceRepositorByWorkspaceUuid_Call struct { + *mock.Call +} + +// GetWorkspaceRepositorByWorkspaceUuid is a helper method to define mock.On call +// - uuid string +func (_e *Database_Expecter) GetWorkspaceRepositorByWorkspaceUuid(uuid interface{}) *Database_GetWorkspaceRepositorByWorkspaceUuid_Call { + return &Database_GetWorkspaceRepositorByWorkspaceUuid_Call{Call: _e.mock.On("GetWorkspaceRepositorByWorkspaceUuid", uuid)} +} + +func (_c *Database_GetWorkspaceRepositorByWorkspaceUuid_Call) Run(run func(uuid string)) *Database_GetWorkspaceRepositorByWorkspaceUuid_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_GetWorkspaceRepositorByWorkspaceUuid_Call) Return(_a0 []db.WorkspaceRepositories) *Database_GetWorkspaceRepositorByWorkspaceUuid_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetWorkspaceRepositorByWorkspaceUuid_Call) RunAndReturn(run func(string) []db.WorkspaceRepositories) *Database_GetWorkspaceRepositorByWorkspaceUuid_Call { + _c.Call.Return(run) + return _c +} + // GetWorkspaceStatusBudget provides a mock function with given fields: workspace_uuid func (_m *Database) GetWorkspaceStatusBudget(workspace_uuid string) db.StatusBudget { ret := _m.Called(workspace_uuid) diff --git a/routes/workspaces.go b/routes/workspaces.go index d79a457bd..d02c6e956 100644 --- a/routes/workspaces.go +++ b/routes/workspaces.go @@ -42,6 +42,9 @@ func WorkspaceRoutes() chi.Router { r.Post("/mission", workspaceHandlers.UpdateWorkspace) r.Post("/tactics", workspaceHandlers.UpdateWorkspace) r.Post("/schematicurl", workspaceHandlers.UpdateWorkspace) + + r.Post("/repositories", workspaceHandlers.CreateWorkspaceRepository) + r.Get("/repositories/{uuid}", workspaceHandlers.GetWorkspaceRepositorByWorkspaceUuid) }) return r } From 7c11757a3d9d0ebfe79acd0e3b3621f20d9988c2 Mon Sep 17 00:00:00 2001 From: elraphty Date: Tue, 14 May 2024 18:37:06 +0100 Subject: [PATCH 03/12] added paginationto feature --- db/features.go | 37 ++++++++++++++++++++--- db/interface.go | 3 +- db/structs.go | 12 ++++---- handlers/features.go | 25 +++++++++++++++- handlers/workspaces.go | 19 ++++-------- mocks/Database.go | 67 +++++++++++++++++++++++++++++++++++------- routes/features.go | 1 + 7 files changed, 129 insertions(+), 35 deletions(-) diff --git a/db/features.go b/db/features.go index c978124be..a4f0aad45 100644 --- a/db/features.go +++ b/db/features.go @@ -1,18 +1,49 @@ package db import ( + "fmt" + "net/http" "strings" "time" + + "github.com/stakwork/sphinx-tribes/utils" ) -func (db database) GetFeaturesByWorkspaceUuid(uuid string) []WorkspaceFeatures { +func (db database) GetFeaturesByWorkspaceUuid(uuid string, r *http.Request) []WorkspaceFeatures { + offset, limit, sortBy, direction, _ := utils.GetPaginationParams(r) + + orderQuery := "" + limitQuery := "" + ms := []WorkspaceFeatures{} - db.db.Model(&WorkspaceFeatures{}).Where("workspace_uuid = ?", uuid).Order("Created").Find(&ms) + if sortBy != "" && direction != "" { + orderQuery = "ORDER BY " + sortBy + " " + direction + } else { + orderQuery = "ORDER BY created DESC" + } + + if limit > 1 { + limitQuery = fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset) + } + + query := `SELECT * FROM public.workspace_features WHERE workspace_uuid = '` + uuid + `'` + + allQuery := query + " " + orderQuery + " " + limitQuery + + theQuery := db.db.Raw(allQuery) + + theQuery.Scan(&ms) return ms } +func (db database) GetWorkspaceFeaturesCount(uuid string) int64 { + var count int64 + db.db.Model(&WorkspaceFeatures{}).Where("workspace_uuid = ?", uuid).Count(&count) + return count +} + func (db database) GetFeatureByUuid(uuid string) WorkspaceFeatures { ms := WorkspaceFeatures{} @@ -35,7 +66,5 @@ func (db database) CreateOrEditFeature(m WorkspaceFeatures) (WorkspaceFeatures, db.db.Create(&m) } - db.db.Model(&WorkspaceFeatures{}).Where("uuid = ?", m.Uuid).Find(&m) - return m, nil } diff --git a/db/interface.go b/db/interface.go index a5c27c430..0124592ad 100644 --- a/db/interface.go +++ b/db/interface.go @@ -143,6 +143,7 @@ type Database interface { CreateWorkspaceRepository(m WorkspaceRepositories) (WorkspaceRepositories, error) GetWorkspaceRepositorByWorkspaceUuid(uuid string) []WorkspaceRepositories CreateOrEditFeature(m WorkspaceFeatures) (WorkspaceFeatures, error) - GetFeaturesByWorkspaceUuid(uuid string) []WorkspaceFeatures + GetFeaturesByWorkspaceUuid(uuid string, r *http.Request) []WorkspaceFeatures + GetWorkspaceFeaturesCount(uuid string) int64 GetFeatureByUuid(uuid string) WorkspaceFeatures } diff --git a/db/structs.go b/db/structs.go index 68547b7fb..06c6100de 100644 --- a/db/structs.go +++ b/db/structs.go @@ -553,9 +553,9 @@ type WorkspaceUsersData struct { type WorkspaceRepositories struct { ID uint `json:"id"` - Uuid string `json:"uuid"` - WorkspaceUuid string `json:"workspace_uuid"` - Name string `json:"name"` + Uuid string `gorm:"not null" json:"uuid"` + WorkspaceUuid string `gorm:"not null" json:"workspace_uuid"` + Name string `gorm:"not null" json:"name"` Url string `json:"url"` Created *time.Time `json:"created"` Updated *time.Time `json:"updated"` @@ -565,9 +565,9 @@ type WorkspaceRepositories struct { type WorkspaceFeatures struct { ID uint `json:"id"` - Uuid string `json:"uuid"` - WorkspaceUuid string `json:"workspace_uuid"` - Name string `json:"name"` + Uuid string `gorm:"not null" json:"uuid"` + WorkspaceUuid string `gorm:"not null" json:"workspace_uuid"` + Name string `gorm:"not null" json:"name"` Brief string `json:"brief"` Requirements string `json:"requirements"` Architecture string `json:"architecture"` diff --git a/handlers/features.go b/handlers/features.go index 9c4f9cd68..ea634cfb0 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/go-chi/chi" + "github.com/rs/xid" "github.com/stakwork/sphinx-tribes/auth" "github.com/stakwork/sphinx-tribes/db" ) @@ -43,6 +44,12 @@ func (oh *featureHandler) CreateOrEditFeatures(w http.ResponseWriter, r *http.Re features.CreatedBy = pubKeyFromAuth + if features.Uuid == "" { + features.Uuid = xid.New().String() + } else { + features.UpdatedBy = pubKeyFromAuth + } + // Validate struct data err = db.Validate.Struct(features) if err != nil { @@ -72,7 +79,23 @@ func (oh *featureHandler) GetFeaturesByWorkspaceUuid(w http.ResponseWriter, r *h } uuid := chi.URLParam(r, "uuid") - workspaceFeatures := oh.db.GetFeaturesByWorkspaceUuid(uuid) + workspaceFeatures := oh.db.GetFeaturesByWorkspaceUuid(uuid, r) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(workspaceFeatures) +} + +func (oh *featureHandler) GetWorkspaceFeaturesCount(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 + } + + uuid := chi.URLParam(r, "uuid") + workspaceFeatures := oh.db.GetWorkspaceFeaturesCount(uuid) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(workspaceFeatures) diff --git a/handlers/workspaces.go b/handlers/workspaces.go index 5175574d3..5ed2f09e6 100644 --- a/handlers/workspaces.go +++ b/handlers/workspaces.go @@ -126,19 +126,6 @@ func (oh *workspaceHandler) CreateOrEditWorkspace(w http.ResponseWriter, r *http } workspace.Name = name } - } else { - // if workspace.ID == 0 { - // // can't create that already exists - // fmt.Println("can't create existing organization") - // w.WriteHeader(http.StatusUnauthorized) - // return - // } - - // if workspace.ID != existing.ID { // can't edit someone else's - // fmt.Println("cant edit another organization") - // w.WriteHeader(http.StatusUnauthorized) - // return - // } } p, err := oh.db.CreateOrEditWorkspace(workspace) @@ -818,6 +805,12 @@ func (oh *workspaceHandler) CreateWorkspaceRepository(w http.ResponseWriter, r * workspaceRepo.CreatedBy = pubKeyFromAuth + if workspaceRepo.Uuid == "" { + workspaceRepo.Uuid = xid.New().String() + } else { + workspaceRepo.UpdatedBy = pubKeyFromAuth + } + // Validate struct data err = db.Validate.Struct(workspaceRepo) if err != nil { diff --git a/mocks/Database.go b/mocks/Database.go index 06093fbe1..6f68771bc 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -2681,17 +2681,17 @@ func (_c *Database_GetFeatureByUuid_Call) RunAndReturn(run func(string) db.Works return _c } -// GetFeaturesByWorkspaceUuid provides a mock function with given fields: uuid -func (_m *Database) GetFeaturesByWorkspaceUuid(uuid string) []db.WorkspaceFeatures { - ret := _m.Called(uuid) +// GetFeaturesByWorkspaceUuid provides a mock function with given fields: uuid, r +func (_m *Database) GetFeaturesByWorkspaceUuid(uuid string, r *http.Request) []db.WorkspaceFeatures { + ret := _m.Called(uuid, r) if len(ret) == 0 { panic("no return value specified for GetFeaturesByWorkspaceUuid") } var r0 []db.WorkspaceFeatures - if rf, ok := ret.Get(0).(func(string) []db.WorkspaceFeatures); ok { - r0 = rf(uuid) + if rf, ok := ret.Get(0).(func(string, *http.Request) []db.WorkspaceFeatures); ok { + r0 = rf(uuid, r) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]db.WorkspaceFeatures) @@ -2708,13 +2708,14 @@ type Database_GetFeaturesByWorkspaceUuid_Call struct { // GetFeaturesByWorkspaceUuid is a helper method to define mock.On call // - uuid string -func (_e *Database_Expecter) GetFeaturesByWorkspaceUuid(uuid interface{}) *Database_GetFeaturesByWorkspaceUuid_Call { - return &Database_GetFeaturesByWorkspaceUuid_Call{Call: _e.mock.On("GetFeaturesByWorkspaceUuid", uuid)} +// - r *http.Request +func (_e *Database_Expecter) GetFeaturesByWorkspaceUuid(uuid interface{}, r interface{}) *Database_GetFeaturesByWorkspaceUuid_Call { + return &Database_GetFeaturesByWorkspaceUuid_Call{Call: _e.mock.On("GetFeaturesByWorkspaceUuid", uuid, r)} } -func (_c *Database_GetFeaturesByWorkspaceUuid_Call) Run(run func(uuid string)) *Database_GetFeaturesByWorkspaceUuid_Call { +func (_c *Database_GetFeaturesByWorkspaceUuid_Call) Run(run func(uuid string, r *http.Request)) *Database_GetFeaturesByWorkspaceUuid_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + run(args[0].(string), args[1].(*http.Request)) }) return _c } @@ -2724,7 +2725,7 @@ func (_c *Database_GetFeaturesByWorkspaceUuid_Call) Return(_a0 []db.WorkspaceFea return _c } -func (_c *Database_GetFeaturesByWorkspaceUuid_Call) RunAndReturn(run func(string) []db.WorkspaceFeatures) *Database_GetFeaturesByWorkspaceUuid_Call { +func (_c *Database_GetFeaturesByWorkspaceUuid_Call) RunAndReturn(run func(string, *http.Request) []db.WorkspaceFeatures) *Database_GetFeaturesByWorkspaceUuid_Call { _c.Call.Return(run) return _c } @@ -4860,6 +4861,52 @@ func (_c *Database_GetWorkspaceByUuid_Call) RunAndReturn(run func(string) db.Wor return _c } +// GetWorkspaceFeaturesCount provides a mock function with given fields: uuid +func (_m *Database) GetWorkspaceFeaturesCount(uuid string) int64 { + ret := _m.Called(uuid) + + if len(ret) == 0 { + panic("no return value specified for GetWorkspaceFeaturesCount") + } + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(uuid) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Database_GetWorkspaceFeaturesCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceFeaturesCount' +type Database_GetWorkspaceFeaturesCount_Call struct { + *mock.Call +} + +// GetWorkspaceFeaturesCount is a helper method to define mock.On call +// - uuid string +func (_e *Database_Expecter) GetWorkspaceFeaturesCount(uuid interface{}) *Database_GetWorkspaceFeaturesCount_Call { + return &Database_GetWorkspaceFeaturesCount_Call{Call: _e.mock.On("GetWorkspaceFeaturesCount", uuid)} +} + +func (_c *Database_GetWorkspaceFeaturesCount_Call) Run(run func(uuid string)) *Database_GetWorkspaceFeaturesCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_GetWorkspaceFeaturesCount_Call) Return(_a0 int64) *Database_GetWorkspaceFeaturesCount_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetWorkspaceFeaturesCount_Call) RunAndReturn(run func(string) int64) *Database_GetWorkspaceFeaturesCount_Call { + _c.Call.Return(run) + return _c +} + // GetWorkspaceInvoices provides a mock function with given fields: workspace_uuid func (_m *Database) GetWorkspaceInvoices(workspace_uuid string) []db.NewInvoiceList { ret := _m.Called(workspace_uuid) diff --git a/routes/features.go b/routes/features.go index 0b2449399..a6189793a 100644 --- a/routes/features.go +++ b/routes/features.go @@ -16,6 +16,7 @@ func FeatureRoutes() chi.Router { r.Post("/", featureHandlers.CreateOrEditFeatures) r.Get("/forworkspace/{uuid}", featureHandlers.GetFeaturesByWorkspaceUuid) r.Get("/{uuid}", featureHandlers.GetFeatureByUuid) + r.Get("/workspace/count/{uuid}", featureHandlers.GetWorkspaceFeaturesCount) }) return r } From 133800dc09320a46607bdbe539553df83b073f2b Mon Sep 17 00:00:00 2001 From: l Date: Fri, 10 May 2024 19:46:30 -0400 Subject: [PATCH 04/12] Added Tests --- cypress/e2e/06_phases.cy.js | 116 +++++++++++++++++++++++++++++ cypress/support/objects/objects.ts | 6 +- 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/06_phases.cy.js 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/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 From f745cdd11ee82e14d8321761c0ce57ed4f379bb6 Mon Sep 17 00:00:00 2001 From: elraphty Date: Wed, 15 May 2024 06:38:45 +0100 Subject: [PATCH 05/12] fixed merge conflicts --- cypress/e2e/06_phases.cy.js | 34 +++++++++-------- db/config.go | 1 + db/features.go | 50 +++++++++++++++++++++++++ db/interface.go | 4 ++ db/structs.go | 11 ++++++ handlers/features.go | 73 +++++++++++++++++++++++++++++++++++-- routes/features.go | 8 +++- 7 files changed, 162 insertions(+), 19 deletions(-) diff --git a/cypress/e2e/06_phases.cy.js b/cypress/e2e/06_phases.cy.js index d74b0bbf2..bcc7dbd32 100644 --- a/cypress/e2e/06_phases.cy.js +++ b/cypress/e2e/06_phases.cy.js @@ -35,7 +35,7 @@ describe('Modify phases name', () => { }).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('name').and.equal(Phases[i].name.trim() + " _addtext"); expect(body).to.have.property('priority').and.equal(Phases[i].priority); }); } @@ -50,15 +50,18 @@ describe('Get phases for feature', () => { method: 'GET', url: `${HostName}/features/${Phases[0].feature_uuid}/phase`, headers: { 'x-jwt': `${ value }` }, - body: {} + 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); - } + + 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); + }); }) }) }) @@ -72,13 +75,13 @@ describe('Get phase by uuid', () => { method: 'GET', url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[i].uuid}`, headers: { 'x-jwt': `${ value }` }, - body: {} + 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); + 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); }) } }) @@ -92,7 +95,7 @@ describe('Delete phase by uuid', () => { method: 'DELETE', url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, headers: { 'x-jwt': `${ value }` }, - body: {} + body: {} }).then((resp) => { expect(resp.status).to.eq(200) }) @@ -107,7 +110,8 @@ describe('Check delete by uuid', () => { method: 'GET', url: `${HostName}/features/${Phases[0].feature_uuid}/phase/${Phases[0].uuid}`, headers: { 'x-jwt': `${ value }` }, - body: {} + body: {}, + failOnStatusCode: false }).then((resp) => { expect(resp.status).to.eq(404); }) 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..8b628fca4 100644 --- a/db/features.go +++ b/db/features.go @@ -1,8 +1,12 @@ package db import ( +<<<<<<< HEAD "fmt" "net/http" +======= + "errors" +>>>>>>> b77bffb1 (tests for Add Phases to Features) "strings" "time" @@ -68,3 +72,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) GetFeaturePhasesByFeatureUuid(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..97ce145d0 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) + GetFeaturePhasesByFeatureUuid(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..e81e71671 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,71 @@ 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 + } + + newPhase.CreatedBy = 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) { + uuid := chi.URLParam(r, "feature_uuid") + phases := oh.db.GetFeaturePhasesByFeatureUuid(uuid) + + 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/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 } From 7a2e1dffbbbb61b085e1c12a0aa68368e6febd94 Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 14 May 2024 03:10:25 +0500 Subject: [PATCH 06/12] again push due to error --- db/features.go | 2 +- db/interface.go | 2 +- handlers/features.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/features.go b/db/features.go index 8b628fca4..2ae964533 100644 --- a/db/features.go +++ b/db/features.go @@ -96,7 +96,7 @@ func (db database) CreateOrEditFeaturePhase(phase FeaturePhase) (FeaturePhase, e return phase, nil } -func (db database) GetFeaturePhasesByFeatureUuid(featureUuid string) []FeaturePhase { +func (db database) GetPhasesByFeatureUuid(featureUuid string) []FeaturePhase { phases := []FeaturePhase{} db.db.Model(&FeaturePhase{}).Where("feature_uuid = ?", featureUuid).Order("Created ASC").Find(&phases) return phases diff --git a/db/interface.go b/db/interface.go index 97ce145d0..7253b9f9d 100644 --- a/db/interface.go +++ b/db/interface.go @@ -147,7 +147,7 @@ type Database interface { GetWorkspaceFeaturesCount(uuid string) int64 GetFeatureByUuid(uuid string) WorkspaceFeatures CreateOrEditFeaturePhase(phase FeaturePhase) (FeaturePhase, error) - GetFeaturePhasesByFeatureUuid(featureUUID string) []FeaturePhase + GetPhasesByFeatureUuid(featureUUID string) []FeaturePhase GetFeaturePhaseByUuid(featureUUID, phaseUUID string) (FeaturePhase, error) DeleteFeaturePhase(featureUUID, phaseUUID string) error } diff --git a/handlers/features.go b/handlers/features.go index e81e71671..ed31eb5d0 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -149,7 +149,7 @@ func (oh *featureHandler) CreateOrEditFeaturePhase(w http.ResponseWriter, r *htt func (oh *featureHandler) GetFeaturePhases(w http.ResponseWriter, r *http.Request) { uuid := chi.URLParam(r, "feature_uuid") - phases := oh.db.GetFeaturePhasesByFeatureUuid(uuid) + phases := oh.db.GetPhasesByFeatureUuid(uuid) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(phases) From 6cf499db7b325af8142fa3252ae045b58f76d1ff Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 14 May 2024 04:11:53 +0500 Subject: [PATCH 07/12] fixed unit test errors --- db/interface.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/interface.go b/db/interface.go index 7253b9f9d..1643e352c 100644 --- a/db/interface.go +++ b/db/interface.go @@ -147,7 +147,7 @@ type Database interface { 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 + GetPhasesByFeatureUuid(featureUuid string) []FeaturePhase + GetFeaturePhaseByUuid(featureUuid, phaseUuid string) (FeaturePhase, error) + DeleteFeaturePhase(featureUuid, phaseUuid string) error } From 32a45ddabeb69140868acb382938e08c768c1cec Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 14 May 2024 04:12:08 +0500 Subject: [PATCH 08/12] fixed unit test errors --- handlers/features.go | 16 +++++----- mocks/Database.go | 72 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/handlers/features.go b/handlers/features.go index ed31eb5d0..aae7ff398 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -148,18 +148,18 @@ func (oh *featureHandler) CreateOrEditFeaturePhase(w http.ResponseWriter, r *htt } func (oh *featureHandler) GetFeaturePhases(w http.ResponseWriter, r *http.Request) { - uuid := chi.URLParam(r, "feature_uuid") - phases := oh.db.GetPhasesByFeatureUuid(uuid) + 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") + featureUuid := chi.URLParam(r, "feature_uuid") + phaseUuid := chi.URLParam(r, "phase_uuid") - phase, err := oh.db.GetFeaturePhaseByUuid(featureUUID, phaseUUID) + phase, err := oh.db.GetFeaturePhaseByUuid(featureUuid, phaseUuid) if err != nil { w.WriteHeader(http.StatusNotFound) return @@ -170,10 +170,10 @@ func (oh *featureHandler) GetFeaturePhaseByUUID(w http.ResponseWriter, r *http.R } func (oh *featureHandler) DeleteFeaturePhase(w http.ResponseWriter, r *http.Request) { - featureUUID := chi.URLParam(r, "feature_uuid") - phaseUUID := chi.URLParam(r, "phase_uuid") + featureUuid := chi.URLParam(r, "feature_uuid") + phaseUuid := chi.URLParam(r, "phase_uuid") - err := oh.db.DeleteFeaturePhase(featureUUID, phaseUUID) + err := oh.db.DeleteFeaturePhase(featureUuid, phaseUuid) if err != nil { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) 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 +} From 3c7cc872b8fe1f4217b310f72d769e4c4c6afb6b Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Tue, 14 May 2024 22:58:04 +0500 Subject: [PATCH 09/12] update code to fulfill requirements --- handlers/features.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/handlers/features.go b/handlers/features.go index aae7ff398..f9e41a42a 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -134,7 +134,15 @@ func (oh *featureHandler) CreateOrEditFeaturePhase(w http.ResponseWriter, r *htt return } - newPhase.CreatedBy = pubKeyFromAuth + if newPhase.Uuid == "" { + newPhase.Uuid = xid.New().String() + } + + if newPhase.CreatedBy != "" { + newPhase.UpdatedBy = pubKeyFromAuth + } else { + newPhase.CreatedBy = pubKeyFromAuth + } phase, err := oh.db.CreateOrEditFeaturePhase(newPhase) if err != nil { From d1f84285b6d7b998d8ea9610ba19eadd660501a6 Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Wed, 15 May 2024 03:55:38 +0500 Subject: [PATCH 10/12] update code logic --- handlers/features.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/handlers/features.go b/handlers/features.go index f9e41a42a..1e2096623 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -138,12 +138,14 @@ func (oh *featureHandler) CreateOrEditFeaturePhase(w http.ResponseWriter, r *htt newPhase.Uuid = xid.New().String() } - if newPhase.CreatedBy != "" { - newPhase.UpdatedBy = pubKeyFromAuth - } else { + 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) From 8f304852b4133f53479ae0689beca7b237d4709e Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Wed, 15 May 2024 05:18:28 +0500 Subject: [PATCH 11/12] change file name --- cypress/e2e/{06_phases.cy.js => 06_phases.cy.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cypress/e2e/{06_phases.cy.js => 06_phases.cy.ts} (100%) diff --git a/cypress/e2e/06_phases.cy.js b/cypress/e2e/06_phases.cy.ts similarity index 100% rename from cypress/e2e/06_phases.cy.js rename to cypress/e2e/06_phases.cy.ts From 50cc848661f726b08618ca48e935a5e52ed8329e Mon Sep 17 00:00:00 2001 From: elraphty Date: Wed, 15 May 2024 06:40:35 +0100 Subject: [PATCH 12/12] fixed merge conflicts --- db/features.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/db/features.go b/db/features.go index 2ae964533..1372674f1 100644 --- a/db/features.go +++ b/db/features.go @@ -1,12 +1,9 @@ package db import ( -<<<<<<< HEAD + "errors" "fmt" "net/http" -======= - "errors" ->>>>>>> b77bffb1 (tests for Add Phases to Features) "strings" "time"