Second commit

added few test, first api to add book
This commit is contained in:
2025-09-23 17:16:48 +02:00
parent 0457ca2011
commit 8432902df1
19 changed files with 298 additions and 123 deletions

76
api_test.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"git.artlef.fr/PersonalLibraryManager/internal/config"
"github.com/stretchr/testify/assert"
)
func testSetup() *gin.Engine {
c := config.LoadConfig("config_test/test.toml")
return setup(&c)
}
func TestGetBooksHandler(t *testing.T) {
router := testSetup()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/books", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
func TestPostBookHandler_Ok(t *testing.T) {
bookJson :=
`{
"title": "Le château",
"author": "Kafka",
"rating": 9
}`
testPostBookHandler(t, bookJson, 200)
}
func TestPostBookHandler_OkOnlyTitle(t *testing.T) {
bookJson :=
`{
"title": "Le château"
}`
testPostBookHandler(t, bookJson, 200)
}
func TestPostBookHandler_noTitle(t *testing.T) {
bookJson :=
`{
"author": "Kafka",
"rating": 9
}`
testPostBookHandler(t, bookJson, 400)
}
func TestPostBookHandler_WrongRating(t *testing.T) {
bookJson :=
`{
"title": "Le château",
"author": "Kafka",
"rating": 15
}`
testPostBookHandler(t, bookJson, 400)
}
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) {
router := testSetup()
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/book",
strings.NewReader(string(bookJson)))
router.ServeHTTP(w, req)
assert.Equal(t, expectedCode, w.Code)
}

9
config_test/test.toml Normal file
View File

@@ -0,0 +1,9 @@
# Path to sqlite database file.
database_file_path = "file::memory:?cache=shared"
# The path to the sql file to load for demo data.
demo_data_path = ""
# The port to listen on for the server.
port = "8080"

View File

@@ -8,7 +8,8 @@
"name": "personal-library-manager",
"version": "0.0.0",
"dependencies": {
"vue": "^3.5.18"
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
@@ -1682,6 +1683,12 @@
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/devtools-core": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.1.tgz",
@@ -3989,6 +3996,21 @@
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -14,7 +14,8 @@
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.5.18"
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.31.0",

41
front/src/AddBook.vue Normal file
View File

@@ -0,0 +1,41 @@
<script setup>
import { ref } from 'vue'
import { postBook } from './api.js'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter();
const book = ref({
title: "",
author: ""
});
function onSubmit(e) {
postBook(book);
router.push('/');
}
</script>
<template>
<form @submit.prevent="onSubmit">
<div class="field">
<label class="label">Title</label>
<div class="control">
<input class="input" type="text" v-model="book.title" placeholder="Title">
</div>
</div>
<div class="field">
<label class="label">Author</label>
<div class="control">
<input class="input" type="text" v-model="book.author" placeholder="Author">
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-link">Submit</button>
</div>
</div>
</form>
</template>
<style scoped></style>

View File

@@ -1,6 +1,6 @@
<script setup>
import BooksBrowser from './BooksBrowser.vue'
import AppNavBar from './AppNavBar.vue'
import { RouterView } from 'vue-router'
</script>
<template>
@@ -8,7 +8,7 @@
<AppNavBar/>
</header>
<main class="section">
<BooksBrowser/>
<RouterView />
</main>
</template>

View File

@@ -1,4 +1,6 @@
<script setup></script>
<script setup>
import { RouterLink } from 'vue-router'
</script>
<template>
<nav class="navbar">
@@ -9,9 +11,12 @@
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">
<RouterLink to="/" class="navbar-item" activeClass="is-active">
Home
</a>
</RouterLink>
<RouterLink to="/add" class="navbar-item" activeClass="is-active">
Add Book
</RouterLink>
<a class="navbar-item">
Books
</a>

View File

@@ -22,7 +22,7 @@ const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath ===
<div class="media-content">
<div class="is-size-4">{{title}}</div>
<div class="is-size-5 is-italic">{{author}}</div>
<p>{{rating}}/5</p>
<p>{{rating}}/10</p>
</div>
</div>
</div>
@@ -37,7 +37,8 @@ img {
}
.box {
transition:ease-in-out 0.04s
transition:ease-in-out 0.04s;
margin-bottom: 15px;
}
.box:hover {

View File

@@ -1,72 +1,38 @@
<script setup>
import BookCard from './BookCard.vue';
import { useFetch } from './fetch.js'
import { getBooks } from './api.js'
const { data, error } = useFetch('http://localhost:8080/books');
const { data, error } = getBooks();
console.log(data.value);
let books = [];
const dumas = {
id: 1,
title: "Le Comte de Monte-Cristo",
author: "Alexandre Dumas",
imagePath: "./comtemontecristo.png",
rating: 4.5
};
const celine = {
id: 2,
title: "Guerre",
author: "Céline",
imagePath: "",
rating: 5
}
const proust = {
id: 3,
title: "La Prisonnière",
author: "Marcel Proust",
imagePath: "",
rating: 5
}
const proust2 = {
id: 4,
title: "Du côté de chez Swann",
author: "Marcel Proust",
imagePath: "",
rating: 5
}
const balzac = {
id: 5,
title: "Splendeurs et misères des courtisanes",
author: "Balzac",
imagePath: "",
rating: 3
}
const cervantes = {
id: 6,
title: "LIngénieux Hidalgo Don Quichotte de la Manche ou L'Ingénieux Noble Don Quichotte de la Manche",
author: "Cervantès",
imagePath: "",
rating: 3
}
books.push(dumas);
books.push(celine);
books.push(proust);
books.push(proust2);
books.push(balzac);
books.push(cervantes);
</script>
<template>
<div class="grid">
<div class="books">
<div v-if="error">Error when loading books: {{ error.message }}</div>
<div class="cell" v-else-if="data" v-for="book in data" :key="book.id">
<div class="book" v-else-if="data" v-for="book in data" :key="book.id">
<BookCard v-bind="book" />
</div>
<div v-else>Loading...</div>
</div>
</template>
<style scoped></style>
<style scoped>
.books {
position:relative;
float:left;
width:100%; height:auto;
padding-bottom: 100px;
line-height: 2.5;
column-count: 4;
column-gap: 15px;
}
#book {
width: 100% !important;
height: auto !important;
transition: ease-in-out 0.15s;
}
</style>

29
front/src/api.js Normal file
View File

@@ -0,0 +1,29 @@
import { ref } from 'vue'
const baseUrl = "http://localhost:8080"
function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
export function getBooks() {
return useFetch(baseUrl + '/books');
}
export function postBook(book) {
fetch(baseUrl + '/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(book.value)
});
}

View File

@@ -1,13 +0,0 @@
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}

View File

@@ -1,4 +1,18 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import BooksBrowser from './BooksBrowser.vue'
import AddBook from './AddBook.vue'
createApp(App).mount('#app')
const routes = [
{ path: '/', component: BooksBrowser },
{ path: '/add', component: AddBook },
]
export const router = createRouter({
history: createWebHistory(),
routes,
})
createApp(App).use(router).mount('#app')

3
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/pelletier/go-toml v1.9.5
github.com/stretchr/testify v1.10.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0
)
@@ -15,6 +16,7 @@ require (
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -32,6 +34,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.21.0 // indirect

7
internal/api/dto.go Normal file
View File

@@ -0,0 +1,7 @@
package api
type bookPostCreate struct {
Title string `json:"title" binding:"required"`
Author string `json:"author"`
Rating int `json:"rating" binding:"min=0,max=10"`
}

9
internal/api/mapper.go Normal file
View File

@@ -0,0 +1,9 @@
package api
import "git.artlef.fr/PersonalLibraryManager/internal/model"
func (b bookPostCreate) toBook() model.Book {
return model.Book{Title: b.Title,
Author: b.Author,
Rating: b.Rating}
}

27
internal/api/routes.go Normal file
View File

@@ -0,0 +1,27 @@
package api
import (
"net/http"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func GetBooksHanderl(c *gin.Context, db *gorm.DB) {
var books []model.Book
db.Model(&model.Book{}).Find(&books)
c.JSON(http.StatusOK, books)
}
func PostBookHandler(c *gin.Context, db *gorm.DB) {
var book bookPostCreate
err := c.ShouldBindJSON(&book)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bookDb := book.toBook()
db.Model(&model.Book{}).Save(&bookDb)
c.String(200, "Success")
}

View File

@@ -9,16 +9,15 @@ import (
)
type Config struct {
Port string `toml:"port" comment:"The port to listen on for the server."`
DemoDataPath string `toml:"demo_data_path" comment:"The path to the sql file to load for demo data."`
Port string `toml:"port" comment:"The port to listen on for the server."`
DatabaseFilePath string `toml:"database_file_path" comment:"Path to sqlite database file."`
DemoDataPath string `toml:"demo_data_path" comment:"The path to the sql file to load for demo data."`
}
func defaultConfig() Config {
return Config{Port: "8080", DemoDataPath: ""}
return Config{Port: "8080", DatabaseFilePath: "plm.db", DemoDataPath: ""}
}
func LoadConfig(configPath string) Config {
f, err := os.ReadFile(configPath)
if err != nil {

View File

@@ -1,7 +1,6 @@
package db
import (
"fmt"
"log"
"os"
@@ -11,12 +10,10 @@ import (
"git.artlef.fr/PersonalLibraryManager/internal/model"
)
func Initdb(databaseDir string, demoDataPath string) *gorm.DB {
createDbFolderIfMissing(databaseDir)
func Initdb(databasePath string, demoDataPath string) *gorm.DB {
db, err := gorm.Open(
sqlite.Open(
fmt.Sprintf(
"%s/plm.db", databaseDir)), &gorm.Config{})
databasePath), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
@@ -38,20 +35,3 @@ func migrateSchema(db *gorm.DB, demoDataPath string) {
}
db.Exec(string(data))
}
func createDbFolderIfMissing(databaseDir string) {
_, openFileErr := os.Open(databaseDir)
if os.IsNotExist(openFileErr) {
createNonExistingDbFolder(databaseDir)
} else if openFileErr != nil {
log.Fatal(openFileErr)
}
}
func createNonExistingDbFolder(databaseDir string) {
log.Printf("Creating missing folder %s\n", databaseDir)
err := os.MkdirAll(databaseDir, 0700)
if err != nil {
log.Fatal(err)
}
}

31
main.go
View File

@@ -1,30 +1,29 @@
package main
import (
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.artlef.fr/PersonalLibraryManager/internal/api"
"git.artlef.fr/PersonalLibraryManager/internal/config"
"git.artlef.fr/PersonalLibraryManager/internal/db"
"git.artlef.fr/PersonalLibraryManager/internal/model"
)
func GetBookHandler(c *gin.Context, db *gorm.DB) {
var books []model.Book
db.Model(&model.Book{}).Find(&books)
c.JSON(http.StatusOK, books)
}
func main() {
c := config.LoadConfig("plm.toml")
db := db.Initdb(".", c.DemoDataPath)
r := gin.Default()
r.Use(cors.Default()) // All origins allowed by default
r.GET("/books", func(c *gin.Context) {
GetBookHandler(c, db)
})
r := setup(&c)
r.Run(":" + c.Port)
}
func setup(config *config.Config) *gin.Engine {
db := db.Initdb(config.DatabaseFilePath, config.DemoDataPath)
r := gin.Default()
r.Use(cors.Default()) // All origins allowed by default
r.GET("/books", func(c *gin.Context) {
api.GetBooksHanderl(c, db)
})
r.POST("/book", func(c *gin.Context) {
api.PostBookHandler(c, db)
})
return r
}