From 475d5a5b03c5b8761830bc97fade216a6859a392 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Fri, 22 May 2026 11:23:04 +0200 Subject: [PATCH] Implement list projects Signed-off-by: Nikolai Rodionov --- Taskfile.yml | 8 ++ internal/repository/projects.go | 47 ++++++- internal/repository/projects_test.go | 123 +++++++++++++++++- .../20260518193048_projects_init.down.sql | 2 +- ...518193057_project_membership_init.down.sql | 10 +- 5 files changed, 172 insertions(+), 18 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 9ad24e3..ab65e2c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -69,6 +69,14 @@ tasks: deps: - migrate + down-migrations: + desc: Roll back all migrations + vars: + SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable + cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} down -all" + deps: + - migrate + drop-migrations: desc: Drop migrations vars: diff --git a/internal/repository/projects.go b/internal/repository/projects.go index d86c816..fa8530e 100644 --- a/internal/repository/projects.go +++ b/internal/repository/projects.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "errors" - "fmt" "time" "github.com/jackc/pgerrcode" @@ -54,6 +53,7 @@ func CreateProject(ctx context.Context, db *sql.DB, data *ProjectData) error { return err } + // When a project is created, we need to insert the default owner project membership queryMembership := ` INSERT INTO project_membership (project_uuid, user_uuid, role, status, invited_by, joined_at) @@ -104,7 +104,6 @@ func GetProjectByID(ctx context.Context, db *sql.DB, projectID string) (*Project } return nil, err } - fmt.Println(data) return data, nil } @@ -125,12 +124,46 @@ func UpdateProject(ctx context.Context, db *sql.DB, data *ProjectData) error { } // ListProjects get all projects that are available for the user from the database -func ListProjects(ctx context.Context, db *sql.DB) ([]*ProjectData, error) { +func ListProjects(ctx context.Context, db *sql.DB, userID string) ([]*ProjectData, error) { query := ` SELECT p.uuid, p.name FROM projects p - JOIN project_membership pm ON pm.project_id = p.id - WHERE pm.user_id = ?` - fmt.Println(query) - return nil, nil + JOIN project_membership pm ON pm.project_uuid = p.uuid + WHERE pm.user_uuid = $1` + rows, err := db.QueryContext(ctx, query, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, err + } + result := []*ProjectData{} + + for rows.Next() { + pd := &ProjectData{} + err := rows.Scan(&pd.UUID, &pd.Name) + if err != nil { + return nil, err + } + result = append(result, pd) + } + return result, nil +} + +// GetProjectOwner should return an owner if a project +func GetProjectOwner(ctx context.Context, db *sql.DB, projectID string) (userID string, err error) { + query := ` + SELECT user_uuid + FROM project_membership + WHERE project_uuid = $1 AND role = 'owner'` + + if err := db.QueryRowContext(ctx, query, projectID).Scan( + &userID, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", ErrNotFound + } + return "", err + } + return } diff --git a/internal/repository/projects_test.go b/internal/repository/projects_test.go index 7c5b4eb..738a88a 100644 --- a/internal/repository/projects_test.go +++ b/internal/repository/projects_test.go @@ -1,7 +1,9 @@ package repository_test import ( + "context" "database/sql" + "fmt" "testing" "time" @@ -10,8 +12,35 @@ import ( "github.com/stretchr/testify/assert" ) +func newTestUniqueEmail(prefix string) string { + if prefix == "" { + prefix = "test" + } + + return fmt.Sprintf( + "%s-%d-%s@example.com", + prefix, + time.Now().UnixMilli(), + uuid.NewString(), + ) +} + +func accountForProject(ctx context.Context) (string, error) { + account := &repository.AccountData{ + UUID: uuid.NewString(), + Email: newTestUniqueEmail("projects"), + PasswordHash: "dummy", + Name: "John", + Surname: "Doe", + } + if err := repository.CreateAccount(ctx, newTestDBConnection(ctx), account); err != nil { + return "", err + } + return account.UUID, nil +} func TestIntegrationProjectsCreate_Success(t *testing.T) { - createdBy := uuid.NewString() + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) project := &repository.ProjectData{ UUID: uuid.NewString(), Name: "test-1", @@ -29,7 +58,8 @@ func TestIntegrationProjectsCreate_Success(t *testing.T) { } func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) { - createdBy := uuid.NewString() + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) project := &repository.ProjectData{ UUID: uuid.NewString(), Name: "test-2", @@ -46,7 +76,8 @@ func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) { } func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) { - createdBy := uuid.NewString() + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) project := &repository.ProjectData{ UUID: uuid.NewString(), Name: "test-3", @@ -65,7 +96,8 @@ func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) { } func TestIntegrationProjectsCreateAndGet_Success(t *testing.T) { - createdBy := uuid.NewString() + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) project := &repository.ProjectData{ UUID: uuid.NewString(), Name: "test-4", @@ -96,7 +128,8 @@ func TestIntegrationProjectsCreateAndGet_NotFound(t *testing.T) { } func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) { - createdBy := uuid.NewString() + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) project := &repository.ProjectData{ UUID: uuid.NewString(), Name: "test-5", @@ -131,3 +164,83 @@ func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) { assert.Equal(t, project.Slug, data.Slug) assert.Equal(t, project.Description, data.Description) } + +func TestIntegrationProjectsCreateAndGetOwner_Success(t *testing.T) { + createdBy, err := accountForProject(t.Context()) + assert.NoError(t, err) + project := &repository.ProjectData{ + UUID: uuid.NewString(), + Name: "test-6", + Slug: uuid.NewString(), + Description: "Test Project Number 1", + CreatedBy: createdBy, + CreatedAt: time.Now(), + ClosedAt: sql.NullTime{Time: time.Now()}, + Blocked: false, + UpdatedAt: time.Now(), + UpdatedBy: createdBy, + } + + assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project)) + userID, err := repository.GetProjectOwner(t.Context(), newTestDBConnection(t.Context()), project.UUID) + assert.NoError(t, err) + assert.Equal(t, project.CreatedBy, userID) +} + +func TestIntegrationListProjectsWorkflow(t *testing.T) { + createdBy1, err := accountForProject(t.Context()) + assert.NoError(t, err) + project1 := &repository.ProjectData{ + UUID: uuid.NewString(), + Name: "List 1", + Slug: uuid.NewString(), + Description: "Test Project Number 1", + CreatedBy: createdBy1, + CreatedAt: time.Now(), + ClosedAt: sql.NullTime{Time: time.Now()}, + Blocked: false, + UpdatedAt: time.Now(), + UpdatedBy: createdBy1, + } + + assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project1)) + + project2 := &repository.ProjectData{ + UUID: uuid.NewString(), + Name: "List 2", + Slug: uuid.NewString(), + Description: "Test Project Number 1", + CreatedBy: createdBy1, + CreatedAt: time.Now(), + ClosedAt: sql.NullTime{Time: time.Now()}, + Blocked: false, + UpdatedAt: time.Now(), + UpdatedBy: createdBy1, + } + assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project2)) + + createdBy2, err := accountForProject(t.Context()) + assert.NoError(t, err) + project3 := &repository.ProjectData{ + UUID: uuid.NewString(), + Name: "List 3", + Slug: uuid.NewString(), + Description: "Test Project Number 1", + CreatedBy: createdBy2, + CreatedAt: time.Now(), + ClosedAt: sql.NullTime{Time: time.Now()}, + Blocked: false, + UpdatedAt: time.Now(), + UpdatedBy: createdBy2, + } + + assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project3)) + + projects, err := repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy1) + assert.NoError(t, err) + assert.Len(t, projects, 2) + + projects, err = repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy2) + assert.NoError(t, err) + assert.Len(t, projects, 1) +} diff --git a/migrations/20260518193048_projects_init.down.sql b/migrations/20260518193048_projects_init.down.sql index 2e30072..48a1f84 100644 --- a/migrations/20260518193048_projects_init.down.sql +++ b/migrations/20260518193048_projects_init.down.sql @@ -1 +1 @@ -DROP TABLE IF EXIST prohects; +DROP TABLE projects; diff --git a/migrations/20260518193057_project_membership_init.down.sql b/migrations/20260518193057_project_membership_init.down.sql index cd338a6..989fcb8 100644 --- a/migrations/20260518193057_project_membership_init.down.sql +++ b/migrations/20260518193057_project_membership_init.down.sql @@ -1,7 +1,7 @@ -DROP INDEX IF EXISTS idx_project_membership_project; -DROP INDEX IF EXISTS idx_project_membership_user; +DROP INDEX idx_project_membership_project; +DROP INDEX idx_project_membership_user; -DROP TABLE IF EXISTS project_membership; +DROP TABLE project_membership; -DROP TYPE IF EXISTS membership_status; -DROP TYPE IF EXISTS project_role; +DROP TYPE membership_status CASCADE; +DROP TYPE project_role CASCADE;