Added plan of tests

This commit is contained in:
Alexander Pivkin
2025-12-20 10:21:45 +03:00
parent a889e1c7dd
commit 640a6d2a46
171 changed files with 57454 additions and 0 deletions
@@ -0,0 +1,131 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gosimple/slug"
"github.com/xNok/go-rest-demo/pkg/recipes"
)
func main() {
// Create Gin router
router := gin.Default()
// Instantiate recipe Handler and provide a data store
store := recipes.NewMemStore()
recipesHandler := NewRecipesHandler(store)
// Register Routes
router.GET("/", homePage)
router.GET("/recipes", recipesHandler.ListRecipes)
router.POST("/recipes", recipesHandler.CreateRecipe)
router.GET("/recipes/:id", recipesHandler.GetRecipe)
router.PUT("/recipes/:id", recipesHandler.UpdateRecipe)
router.DELETE("/recipes/:id", recipesHandler.DeleteRecipe)
// Start the server
router.Run()
}
func homePage(c *gin.Context) {
c.String(http.StatusOK, "This is my home page")
}
type RecipesHandler struct {
store recipeStore
}
func NewRecipesHandler(s recipeStore) *RecipesHandler {
return &RecipesHandler{
store: s,
}
}
type recipeStore interface {
Add(name string, recipe recipes.Recipe) error
Get(name string) (recipes.Recipe, error)
List() (map[string]recipes.Recipe, error)
Update(name string, recipe recipes.Recipe) error
Remove(name string) error
}
func (h RecipesHandler) CreateRecipe(c *gin.Context) {
// Get request body and convert it to recipes.Recipe
var recipe recipes.Recipe
if err := c.ShouldBindJSON(&recipe); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// create a url friendly name
id := slug.Make(recipe.Name)
// add to the store
h.store.Add(id, recipe)
// return success payload
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
func (h RecipesHandler) ListRecipes(c *gin.Context) {
r, err := h.store.List()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
c.JSON(200, r)
}
func (h RecipesHandler) GetRecipe(c *gin.Context) {
id := c.Param("id")
recipe, err := h.store.Get(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
}
c.JSON(200, recipe)
}
func (h RecipesHandler) UpdateRecipe(c *gin.Context) {
// Get request body and convert it to recipes.Recipe
var recipe recipes.Recipe
if err := c.ShouldBindJSON(&recipe); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id := c.Param("id")
err := h.store.Update(id, recipe)
if err != nil {
if err == recipes.NotFoundErr {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// return success payload
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
func (h RecipesHandler) DeleteRecipe(c *gin.Context) {
id := c.Param("id")
err := h.store.Remove(id)
if err != nil {
if err == recipes.NotFoundErr {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// return success payload
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
@@ -0,0 +1,99 @@
###
GET http://localhost:8080/
###
POST http://localhost:8080/recipes
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
}
]
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8080/recipes
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(JSON.stringify(response.body) === "{\"ham-and-cheese-toasties\":{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}}", "Body match expected response")
});
%}
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(JSON.stringify(response.body) === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}", "Body match expected response")
});
%}
###
PUT http://localhost:8080/recipes/ham-and-cheese-toasties
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
},{
"name": "butter"
}
]
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(JSON.stringify(response.body) === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"},{\"name\":\"butter\"}]}", "Body match expected response")
});
%}
###
DELETE http://localhost:8080/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 404, "Response status is not 404");
});
%}
@@ -0,0 +1,159 @@
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/gosimple/slug"
"github.com/xNok/go-rest-demo/pkg/recipes"
)
func main() {
// create the Store and Recipe Handler
store := recipes.NewMemStore()
recipesHandler := NewRecipesHandler(store)
home := homeHandler{}
router := mux.NewRouter()
router.HandleFunc("/", home.ServeHTTP)
router.HandleFunc("/recipes", recipesHandler.ListRecipes).Methods("GET")
router.HandleFunc("/recipes", recipesHandler.CreateRecipe).Methods("POST")
router.HandleFunc("/recipes/{id}", recipesHandler.GetRecipe).Methods("GET")
router.HandleFunc("/recipes/{id}", recipesHandler.UpdateRecipe).Methods("PUT")
router.HandleFunc("/recipes/{id}", recipesHandler.DeleteRecipe).Methods("DELETE")
http.ListenAndServe(":8010", router)
}
func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 Internal Server Error"))
}
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not Found"))
}
type RecipesHandler struct {
store recipeStore
}
func NewRecipesHandler(s recipeStore) *RecipesHandler {
return &RecipesHandler{
store: s,
}
}
type recipeStore interface {
Add(name string, recipe recipes.Recipe) error
Get(name string) (recipes.Recipe, error)
List() (map[string]recipes.Recipe, error)
Update(name string, recipe recipes.Recipe) error
Remove(name string) error
}
func (h *RecipesHandler) CreateRecipe(w http.ResponseWriter, r *http.Request) {
// Recipe object that will be populated from json payload
var recipe recipes.Recipe
if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
resourceID := slug.Make(recipe.Name)
if err := h.store.Add(resourceID, recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *RecipesHandler) ListRecipes(w http.ResponseWriter, r *http.Request) {
recipes, err := h.store.List()
jsonBytes, err := json.Marshal(recipes)
if err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
func (h *RecipesHandler) GetRecipe(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
recipe, err := h.store.Get(id)
if err != nil {
if err == recipes.NotFoundErr {
NotFoundHandler(w, r)
return
}
InternalServerErrorHandler(w, r)
return
}
jsonBytes, err := json.Marshal(recipe)
if err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
func (h *RecipesHandler) UpdateRecipe(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
// Recipe object that will be populated from json payload
var recipe recipes.Recipe
if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
if err := h.store.Update(id, recipe); err != nil {
if err == recipes.NotFoundErr {
NotFoundHandler(w, r)
return
}
InternalServerErrorHandler(w, r)
return
}
jsonBytes, err := json.Marshal(recipe)
if err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
func (h *RecipesHandler) DeleteRecipe(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
if err := h.store.Remove(id); err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
}
type homeHandler struct{}
func (h *homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is my home page"))
}
@@ -0,0 +1,96 @@
###
POST http://localhost:8010/recipes
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
}
]
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8010/recipes
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body === "{\"ham-and-cheese-toasties\":{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}}", "Body match expected response")
});
%}
###
GET http://localhost:8010/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}", "Body match expected response")
});
%}
###
PUT http://localhost:8010/recipes/ham-and-cheese-toasties
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
},{
"name": "butter"
}
]
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8010/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"},{\"name\":\"butter\"}]}", "Body match expected response")
});
%}
###
DELETE http://localhost:8010/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
###
GET http://localhost:8010/recipes/ham-and-cheese-toasties
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 404, "Response status is not 404");
});
%}
@@ -0,0 +1,194 @@
package main
import (
"encoding/json"
"net/http"
"regexp"
"github.com/gosimple/slug"
"github.com/xNok/go-rest-demo/pkg/recipes"
)
var (
RecipeRe = regexp.MustCompile(`^/recipes/*$`)
RecipeReWithID = regexp.MustCompile(`^/recipes/([a-z0-9]+(?:-[a-z0-9]+)+)$`)
)
func main() {
// create the Store and Recipe Handler
store := recipes.NewMemStore()
recipesHandler := NewRecipesHandler(store)
// Create a new request multiplexer
// Takes incoming requests and dispatch them to the matching handlers
mux := http.NewServeMux()
// Register the routes and handlers
mux.Handle("/", &homeHandler{})
mux.Handle("/recipes", recipesHandler)
mux.Handle("/recipes/", recipesHandler)
// Run the server
http.ListenAndServe(":8080", mux)
}
type homeHandler struct{}
func (h *homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is my home page"))
}
func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 Internal Server Error"))
}
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not Found"))
}
// recipeStore represent a data storage containing recipes
type recipeStore interface {
Add(name string, recipe recipes.Recipe) error
Get(name string) (recipes.Recipe, error)
List() (map[string]recipes.Recipe, error)
Update(name string, recipe recipes.Recipe) error
Remove(name string) error
}
// RecipesHandler implements http.Handler and dispatch request to the store
type RecipesHandler struct {
store recipeStore
}
func NewRecipesHandler(s recipeStore) *RecipesHandler {
return &RecipesHandler{
store: s,
}
}
func (h *RecipesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && RecipeRe.MatchString(r.URL.Path):
h.CreateRecipe(w, r)
return
case r.Method == http.MethodGet && RecipeRe.MatchString(r.URL.Path):
h.ListRecipes(w, r)
return
case r.Method == http.MethodGet && RecipeReWithID.MatchString(r.URL.Path):
h.GetRecipe(w, r)
return
case r.Method == http.MethodPut && RecipeReWithID.MatchString(r.URL.Path):
h.UpdateRecipe(w, r)
return
case r.Method == http.MethodDelete && RecipeReWithID.MatchString(r.URL.Path):
h.DeleteRecipe(w, r)
return
default:
NotFoundHandler(w, r)
return
}
}
func (h *RecipesHandler) CreateRecipe(w http.ResponseWriter, r *http.Request) {
// Recipe object that will be populated from json payload
var recipe recipes.Recipe
if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
resourceID := slug.Make(recipe.Name)
if err := h.store.Add(resourceID, recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *RecipesHandler) ListRecipes(w http.ResponseWriter, r *http.Request) {
recipesList, err := h.store.List()
jsonBytes, err := json.Marshal(recipesList)
if err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
func (h *RecipesHandler) GetRecipe(w http.ResponseWriter, r *http.Request) {
matches := RecipeReWithID.FindStringSubmatch(r.URL.Path)
if len(matches) < 2 {
InternalServerErrorHandler(w, r)
return
}
recipe, err := h.store.Get(matches[1])
if err != nil {
if err == recipes.NotFoundErr {
NotFoundHandler(w, r)
return
}
InternalServerErrorHandler(w, r)
return
}
jsonBytes, err := json.Marshal(recipe)
if err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
func (h *RecipesHandler) UpdateRecipe(w http.ResponseWriter, r *http.Request) {
matches := RecipeReWithID.FindStringSubmatch(r.URL.Path)
if len(matches) < 2 {
InternalServerErrorHandler(w, r)
return
}
// Recipe object that will be populated from JSON payload
var recipe recipes.Recipe
if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil {
InternalServerErrorHandler(w, r)
return
}
if err := h.store.Update(matches[1], recipe); err != nil {
if err == recipes.NotFoundErr {
NotFoundHandler(w, r)
return
}
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *RecipesHandler) DeleteRecipe(w http.ResponseWriter, r *http.Request) {
matches := RecipeReWithID.FindStringSubmatch(r.URL.Path)
if len(matches) < 2 {
InternalServerErrorHandler(w, r)
return
}
if err := h.store.Remove(matches[1]); err != nil {
InternalServerErrorHandler(w, r)
return
}
w.WriteHeader(http.StatusOK)
}
@@ -0,0 +1,92 @@
package main
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xNok/go-rest-demo/pkg/recipes"
)
func readTestData(t *testing.T, name string) []byte {
t.Helper()
content, err := os.ReadFile("../../testdata/" + name)
if err != nil {
t.Errorf("Could not read %v", name)
}
return content
}
func TestRecipesHandlerCRUD_Integration(t *testing.T) {
// Create a MemStore and Recipe Handler
store := recipes.NewMemStore()
recipesHandler := NewRecipesHandler(store)
// Testdata
hamAndCheese := readTestData(t, "ham_and_cheese_recipe.json")
hamAndCheeseReader := bytes.NewReader(hamAndCheese)
hamAndCheeseWithButter := readTestData(t, "ham_and_cheese_with_butter_recipe.json")
hamAndCheeseWithButterReader := bytes.NewReader(hamAndCheeseWithButter)
// CREATE - add a new recipe
req := httptest.NewRequest(http.MethodPost, "/recipes", hamAndCheeseReader)
w := httptest.NewRecorder()
recipesHandler.ServeHTTP(w, req)
res := w.Result()
defer res.Body.Close()
assert.Equal(t, 200, res.StatusCode)
saved, _ := store.List()
assert.Len(t, saved, 1)
// GET - find the record we just added
req = httptest.NewRequest(http.MethodGet, "/recipes/ham-and-cheese-toasties", nil)
w = httptest.NewRecorder()
recipesHandler.ServeHTTP(w, req)
res = w.Result()
defer res.Body.Close()
assert.Equal(t, 200, res.StatusCode)
data, err := io.ReadAll(res.Body)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assert.JSONEq(t, string(hamAndCheese), string(data))
// UPDATE - add butter to ham and cheese recipe
req = httptest.NewRequest(http.MethodPut, "/recipes/ham-and-cheese-toasties", hamAndCheeseWithButterReader)
w = httptest.NewRecorder()
recipesHandler.ServeHTTP(w, req)
res = w.Result()
defer res.Body.Close()
assert.Equal(t, 200, res.StatusCode)
updatedHamAndCheese, err := store.Get("ham-and-cheese-toasties")
assert.NoError(t, err)
assert.Contains(t, updatedHamAndCheese.Ingredients, recipes.Ingredient{Name: "butter"})
//DELETE - remove the ham and cheese recipe
req = httptest.NewRequest(http.MethodDelete, "/recipes/ham-and-cheese-toasties", nil)
w = httptest.NewRecorder()
recipesHandler.ServeHTTP(w, req)
res = w.Result()
defer res.Body.Close()
assert.Equal(t, 200, res.StatusCode)
saved, _ = store.List()
assert.Len(t, saved, 0)
}
@@ -0,0 +1,53 @@
###
GET http://localhost:8080
###
POST http://localhost:8080/recipes/
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
}
]
}
###
GET http://localhost:8080/recipes
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties
###
PUT http://localhost:8080/recipes/ham-and-cheese-toasties
Content-Type: application/json
{
"name": "Ham and cheese toasties",
"ingredients": [
{
"name": "bread"
},{
"name": "ham"
},{
"name": "cheese"
},{
"name": "butter"
}
]
}
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties
###
DELETE http://localhost:8080/recipes/ham-and-cheese-toasties
###
GET http://localhost:8080/recipes/ham-and-cheese-toasties