Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pkg/models/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package models

import "time"

type ReviewModel struct {
Model
Title string `db:"title" json:"title"`
EventID string `db:"event_id" json:"event_id"`
AuthorID string `db:"author_id" json:"author_id"`
Body string `db:"body" json:"body"`
}

// BeforeUpdated overrides model lifecycle hook, updating the updated_at time.
func (m *ReviewModel) BeforeUpdated() error {
m.UpdatedAt = time.Now()
return nil
}
121 changes: 121 additions & 0 deletions pkg/repository/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package repository

import (
"database/sql"
"errors"
"fmt"
"net"
"reflect"

"github.com/BEOpenSourceCollabs/EventManagementCore/pkg/models"
)

type ReviewRepository interface {
CreateReview(review *models.ReviewModel) error
GetReviewByID(id string) (*models.ReviewModel, error)
DeleteReview(id string) error
UpdateReview(review *models.ReviewModel) error
}

type sqlReviewRepository struct {
database *sql.DB
}

func NewSQLReviewRepository(database *sql.DB) ReviewRepository {
return &sqlReviewRepository{database: database}
}

func (r *sqlReviewRepository) handleError(err error) error {
if errors.Is(err, sql.ErrNoRows) {
return ErrReviewNotFound
}
if reflect.TypeOf(err) == reflect.TypeOf(&net.OpError{}) {
return ErrRepoConnErr
}
return err
}

// CreateReview inserts a new review into the database
func (r *sqlReviewRepository) CreateReview(review *models.ReviewModel) error {
review.BeforeCreate()
query := `INSERT INTO public.reviews (title, event_id, author_id, body)
VALUES ($1, $2, $3, $4) RETURNING id`
err := r.database.QueryRow(query,
review.Title,
review.EventID,
review.AuthorID,
review.Body,
).Scan(&review.ID)
if err != nil {
return fmt.Errorf("failed to create review: %w", err)
}
review.AfterCreate()
return nil
}

// GetReviewByID retrieves a review from the database by its unique ID
func (r *sqlReviewRepository) GetReviewByID(id string) (*models.ReviewModel, error) {
query := `SELECT id, title, event_id, author_id, body, created_at, updated_at FROM public.reviews WHERE id = $1`
review := &models.ReviewModel{}
err := r.database.QueryRow(query, id).Scan(
&review.ID,
&review.Title,
&review.EventID,
&review.AuthorID,
&review.Body,
&review.CreatedAt,
&review.UpdatedAt,
)
if err != nil {
return nil, r.handleError(err)
}
return review, nil
}

// UpdateReview updates a review in the database
func (r *sqlReviewRepository) UpdateReview(review *models.ReviewModel) error {
review.BeforeUpdate()
query := `UPDATE public.reviews SET title = $1, event_id = $2, author_id = $3, body = $4 WHERE id = $5`
if review.CreatedAt.Unix() == 0 {
return fmt.Errorf("unable to update a review that was not loaded from the database")
}
rs, err := r.database.Exec(
query,
review.Title,
review.EventID,
review.AuthorID,
review.Body,
review.ID,
)
if err != nil {
return r.handleError(err)
}
if affected, err := rs.RowsAffected(); affected < 1 {
if err != nil {
return err
}
return ErrReviewNotFound
}
review.AfterUpdate()
return nil
}

// DeleteReview deletes a review from the database
func (r *sqlReviewRepository) DeleteReview(id string) error {
query := `DELETE FROM public.reviews WHERE id = $1`
rs, err := r.database.Exec(query, id)
if err != nil {
return r.handleError(err)
}
if affected, err := rs.RowsAffected(); affected < 1 {
if err != nil {
return err
}
return ErrReviewNotFound
}
return nil
}

var (
ErrReviewNotFound = errors.New("review not found") // ErrReviewNotFound is returned when a review is not found in the database.
)
167 changes: 167 additions & 0 deletions pkg/repository/review_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package repository_test

import (
"context"
"database/sql"
"testing"
"time"

"github.com/BEOpenSourceCollabs/EventManagementCore/pkg/models"
"github.com/BEOpenSourceCollabs/EventManagementCore/pkg/repository"
"github.com/BEOpenSourceCollabs/EventManagementCore/pkg/test"
"github.com/BEOpenSourceCollabs/EventManagementCore/pkg/types"
)

var (
reviews = []models.ReviewModel{
{
Title: "Great Event",
Body: "This was an amazing event!",
},
{
Title: "Good Experience",
Body: "I enjoyed this event, but there's room for improvement.",
},
{
Title: "Disappointing",
Body: "The event didn't meet my expectations.",
},
}
)

func TestReviewRepository_KitchenSink(t *testing.T) {
container, db, err := test.NewTestDatabaseWithContainer(test.TestDatabaseConfiguration{
RootRelativePath: "../../",
})
if err != nil {
t.Fatal(err)
}
defer container.Terminate(context.Background())

reviewRepo := repository.NewSQLReviewRepository(db)
userRepo := repository.NewSQLUserRepository(db)
eventRepo := repository.NewSQLEventRepository(db)

author := models.UserModel{
Username: "author0",
FirstName: sql.NullString{
String: "author",
Valid: true,
},
LastName: sql.NullString{
String: "0",
Valid: true,
},
Email: "author0@example.co.uk",
Verified: true,
Role: types.UserRole,
}

organizer := models.UserModel{
Username: "org0",
FirstName: sql.NullString{
String: "org",
Valid: true,
},
LastName: sql.NullString{
String: "0",
Valid: true,
},
Email: "org0@example.co.uk",
Verified: true,
Role: types.OrganizerRole,
}

event := models.EventModel{
Name: "Coding Meetup",
Type: types.BothEventType,
Description: "A test networking event",
StartDate: time.Now().Add(time.Hour + 48),
EndDate: time.Now().Add(time.Hour + 49),
IsPaid: false,
CountryISO: "GB",
City: "London",
}

t.Run("Create review author", func(t *testing.T) {
if err := userRepo.CreateUser(&author); err != nil {
t.Errorf("expected no error when creating review author but got %v", err)
}
})

t.Run("Create event organizer", func(t *testing.T) {
if err := userRepo.CreateUser(&organizer); err != nil {
t.Errorf("expected no error when creating organizer but got %v", err)
}
})

t.Run("Create event", func(t *testing.T) {
event.Organizer = organizer.ID
if err := eventRepo.CreateEvent(&event); err != nil {
t.Errorf("expected no error when creating event but got %v", err)
}
})

t.Run("Create reviews", func(t *testing.T) {
for i := range reviews {
reviews[i].AuthorID = author.ID // FKC requires a valid author id
reviews[i].EventID = event.ID // FKC requires a valid event id

if err := reviewRepo.CreateReview(&reviews[i]); err != nil {
t.Errorf("expected no error when creating review but got %v", err)
}
if reviews[i].ID == "" {
t.Error("expected review ID to be set after creation")
}
}
})

t.Run("Update review without loading first", func(t *testing.T) {
if err := reviewRepo.UpdateReview(&models.ReviewModel{Model: models.Model{ID: reviews[0].ID}, Title: "Updated"}); err == nil {
t.Error("expected error while attempting to update a review without fully loading the review first")
}
})

t.Run("Update review", func(t *testing.T) {
reviews[0].Title = "Updated Title"
reviews[0].Body = "Updated body content"
if err := reviewRepo.UpdateReview(&reviews[0]); err != nil {
t.Errorf("expected no error when updating review but got %v", err)
}
loaded, err := reviewRepo.GetReviewByID(reviews[0].ID)
if err != nil {
t.Errorf("expected no error when getting review by id but got %v", err)
}
if loaded.Title != "Updated Title" {
t.Errorf("expected loaded review's title to be updated to 'Updated Title' but was '%s'", loaded.Title)
}
if loaded.Body != "Updated body content" {
t.Errorf("expected loaded review's body to be updated but was '%s'", loaded.Body)
}
})

t.Run("Get review by ID", func(t *testing.T) {
loaded, err := reviewRepo.GetReviewByID(reviews[1].ID)
if err != nil {
t.Errorf("expected no error when getting review by ID but got %v", err)
}
if loaded.ID != reviews[1].ID {
t.Errorf("expected loaded review's id to match '%s' but was '%s'", reviews[1].ID, loaded.ID)
}
})

t.Run("Delete reviews", func(t *testing.T) {
for _, review := range reviews {
if err := reviewRepo.DeleteReview(review.ID); err != nil {
t.Errorf("expected no error when deleting review but got %v", err)
}
deletedReview, err := reviewRepo.GetReviewByID(review.ID)
if deletedReview != nil {
t.Errorf("expected deleted review to be nil but was %v", deletedReview)
}
if err == nil {
t.Errorf("expected error when attempting to get review by id after deletion")
}
}
})
}