Skip to content

Commit

Permalink
Merge pull request #65 from Ajinx1/ayotee-dev
Browse files Browse the repository at this point in the history
NewsLetter Page Api Endpoint
  • Loading branch information
NwokoyeChigozie authored Jul 21, 2024
2 parents c42f8ea + 8df0eb7 commit 9af545d
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 2 deletions.
1 change: 1 addition & 0 deletions internal/models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ func AuthMigrationModels() []interface{} {
models.Profile{},
models.Product{},
models.User{},
models.NewsLetter{},
} // an array of db models, example: User{}
}

Expand Down
36 changes: 36 additions & 0 deletions internal/models/newsletter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package models

import (
"time"

"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/postgresql"
"github.com/hngprojects/hng_boilerplate_golang_web/utility"
"gorm.io/gorm"
)

type NewsLetter struct {
ID string `gorm:"primaryKey;type:uuid" json:"id"`
Email string `gorm:"unique;not null" json:"email" validate:"required,email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

func (n *NewsLetter) BeforeCreate(tx *gorm.DB) (err error) {

if n.ID == "" {
n.ID = utility.GenerateUUID()
}
return
}

func (c *NewsLetter) CreateNewsLetter(db *gorm.DB) error {

err := postgresql.CreateOneRecord(db, &c)

if err != nil {
return err
}

return nil
}
59 changes: 59 additions & 0 deletions pkg/controller/newsletter/post.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package newsletter

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/hngprojects/hng_boilerplate_golang_web/external/request"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage"
service "github.com/hngprojects/hng_boilerplate_golang_web/services/newsletter"
"github.com/hngprojects/hng_boilerplate_golang_web/utility"
)

type Controller struct {
Db *storage.Database
Validator *validator.Validate
Logger *utility.Logger
ExtReq request.ExternalRequest
}

func (base *Controller) SubscribeNewsLetter(c *gin.Context) {
var (
req = models.NewsLetter{}
)

err := c.ShouldBind(&req)
if err != nil {
rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "Failed to parse request body", err, nil)
c.JSON(http.StatusBadRequest, rd)
return
}

err = base.Validator.Struct(&req)
if err != nil {
rd := utility.BuildErrorResponse(http.StatusUnprocessableEntity, "error", "Validation failed",
utility.ValidationResponse(err, base.Validator), nil)
c.JSON(http.StatusUnprocessableEntity, rd)
return
}

err = service.NewsLetterSubscribe(&req, base.Db.Postgresql)
if err != nil {
if err == service.ErrEmailAlreadySubscribed {
rd := utility.BuildErrorResponse(http.StatusConflict, "error", "Email already subscribed", nil, nil)
c.JSON(http.StatusConflict, rd)
} else {
rd := utility.BuildErrorResponse(http.StatusInternalServerError, "error", "Failed to subscribe", err, nil)
c.JSON(http.StatusInternalServerError, rd)
}
return
}

base.Logger.Info("subscribed successfully")

rd := utility.BuildSuccessResponse(http.StatusCreated, "subscribed successfully", nil)
c.JSON(http.StatusCreated, rd)

}
1 change: 1 addition & 0 deletions pkg/controller/product/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package product
5 changes: 5 additions & 0 deletions pkg/repository/storage/postgresql/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ func DeleteRecordFromDb(db *gorm.DB, record interface{}) error {
tx := db.Delete(record)
return tx.Error
}

func HardDeleteRecordFromDb(db *gorm.DB, record interface{}) error {
tx := db.Unscoped().Delete(record)
return tx.Error
}
23 changes: 23 additions & 0 deletions pkg/router/newsletter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package router

import (
"fmt"

"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/hngprojects/hng_boilerplate_golang_web/external/request"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/newsletter"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage"
"github.com/hngprojects/hng_boilerplate_golang_web/utility"
)

func Newsletter(r *gin.Engine, ApiVersion string, validator *validator.Validate, db *storage.Database, logger *utility.Logger) *gin.Engine {
extReq := request.ExternalRequest{Logger: logger, Test: false}
newsLetter := newsletter.Controller{Db: db, Validator: validator, Logger: logger, ExtReq: extReq}

newsLetterUrl := r.Group(fmt.Sprintf("%v", ApiVersion))
{
newsLetterUrl.POST("/newsletter", newsLetter.SubscribeNewsLetter)
}
return r
}
1 change: 1 addition & 0 deletions pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func Setup(logger *utility.Logger, validator *validator.Validate, db *storage.Da
Seed(r, ApiVersion, validator, db, logger)
User(r, ApiVersion, validator, db, logger)
Organisation(r, ApiVersion, validator, db, logger)
Newsletter(r, ApiVersion, validator, db, logger)

r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
Expand Down
24 changes: 24 additions & 0 deletions services/newsletter/newsletter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package service

import (
"errors"

"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/postgresql"
"gorm.io/gorm"
)

var ErrEmailAlreadySubscribed = errors.New("email already subscribed")

func NewsLetterSubscribe(newsletter *models.NewsLetter, db *gorm.DB) error {

if postgresql.CheckExists(db, newsletter, "email = ?", newsletter.Email) {
return ErrEmailAlreadySubscribed
}

if err := newsletter.CreateNewsLetter(db); err != nil {
return err
}

return nil
}
149 changes: 149 additions & 0 deletions tests/newsletter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package tests

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/hngprojects/hng_boilerplate_golang_web/internal/models"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/newsletter"
"github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage"
)

func setupNewsLetterTestRouter() (*gin.Engine, *newsletter.Controller) {
gin.SetMode(gin.TestMode)

logger := Setup()
db := storage.Connection()
validator := validator.New()

newsController := &newsletter.Controller{
Db: db,
Validator: validator,
Logger: logger,
}

r := gin.Default()
SetupNewsLetterRoutes(r, newsController)
return r, newsController
}

func SetupNewsLetterRoutes(r *gin.Engine, newsController *newsletter.Controller) {
r.POST("/api/v1/newsletter", newsController.SubscribeNewsLetter)
}

func TestE2ENewsletterSubscription(t *testing.T) {
router, _ := setupNewsLetterTestRouter()

// Test POST /newsletter
body := models.NewsLetter{
Email: "[email protected]",
}
jsonBody, err := json.Marshal(body)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}

req, err := http.NewRequest(http.MethodPost, "/api/v1/newsletter", bytes.NewBuffer(jsonBody))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

AssertStatusCode(t, resp.Code, http.StatusCreated)

response := ParseResponse(resp)
AssertResponseMessage(t, response["message"].(string), "subscribed successfully")
}

func TestPostNewsletter_ValidateEmail(t *testing.T) {
router, _ := setupNewsLetterTestRouter()

body := models.NewsLetter{
Email: "invalid-email",
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest(http.MethodPost, "/api/v1/newsletter", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

response := ParseResponse(resp)
AssertStatusCode(t, resp.Code, http.StatusUnprocessableEntity)
AssertResponseMessage(t, response["message"].(string), "Validation failed")
}

func TestPostNewsletter_CheckDuplicateEmail(t *testing.T) {
router, newsController := setupNewsLetterTestRouter()

db := newsController.Db.Postgresql
db.Create(&models.NewsLetter{Email: "[email protected]"})

body := models.NewsLetter{
Email: "[email protected]",
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest(http.MethodPost, "/api/v1/newsletter", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

response := ParseResponse(resp)
AssertStatusCode(t, resp.Code, http.StatusConflict)
AssertResponseMessage(t, response["message"].(string), "Email already subscribed")
}

func TestPostNewsletter_SaveData(t *testing.T) {
router, newsController := setupNewsLetterTestRouter()

body := models.NewsLetter{
Email: "[email protected]",
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest(http.MethodPost, "/api/v1/newsletter", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

response := ParseResponse(resp)
AssertStatusCode(t, resp.Code, http.StatusCreated)
AssertResponseMessage(t, response["message"].(string), "subscribed successfully")

var newsletter models.NewsLetter
newsController.Db.Postgresql.First(&newsletter, "email = ?", "[email protected]")
if newsletter.Email != "[email protected]" {
t.Errorf("data not saved correctly to the database: expected email %s, got %s", "[email protected]", newsletter.Email)
}
}

func TestPostNewsletter_ResponseAndStatusCode(t *testing.T) {
router, _ := setupNewsLetterTestRouter()

body := models.NewsLetter{
Email: "[email protected]",
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest(http.MethodPost, "/api/v1/newsletter", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()

router.ServeHTTP(resp, req)

response := ParseResponse(resp)
AssertStatusCode(t, resp.Code, http.StatusCreated)
AssertResponseMessage(t, response["message"].(string), "subscribed successfully")
}
4 changes: 2 additions & 2 deletions utility/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

type Response struct {
Status string `json:"status,omitempty"`
Code int `json:"code,omitempty"`
StatusCode int `json:"status_code,omitempty"`
Name string `json:"name,omitempty"` //name of the error
Message string `json:"message,omitempty"`
Error interface{} `json:"error,omitempty"` //for errors that occur even if request is successful
Expand Down Expand Up @@ -47,7 +47,7 @@ func ResponseMessage(code int, status string, name string, message string, err i
}

res := Response{
Code: code,
StatusCode: code,
Name: name,
Status: status,
Message: message,
Expand Down

0 comments on commit 9af545d

Please sign in to comment.