13 Commits
0.3.0 ... 0.4.0

31 changed files with 505 additions and 440 deletions

View File

@@ -3,7 +3,7 @@ FROM node:lts AS buildfront
COPY front . COPY front .
RUN npm install && npm run build RUN npm install && npm run build
FROM golang:1.25 AS build FROM golang:1.26 AS build
WORKDIR /src WORKDIR /src
COPY . . COPY . .
COPY --from=buildfront ./dist front/dist COPY --from=buildfront ./dist front/dist

View File

@@ -28,7 +28,7 @@ Or with a volume, for example if you created a volume named `bibliomane_data`:
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`. `--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
The password can be generated using `htpasswd -nB [username]`. The password can be generated using `htpasswd -nBC10 [username]`.
For example, to create an user account `demo`: For example, to create an user account `demo`:

View File

@@ -69,7 +69,7 @@ INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW'
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg')); INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg'));
INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0); INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0);
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.jpg')); INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.jpg'));
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'D''un château l''autre'), true,10); INSERT INTO user_books(created_at, user_id, book_id, read, rating, review) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'D''un château l''autre'), true,10, "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.jpg')); INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.jpg'));
INSERT INTO user_books(created_at, user_id, book_id, read, start_read_date, end_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Les dieux ont soif'), true,'2026-01-30 00:00:00+00:00','2026-02-13 00:00:00+00:00',7); INSERT INTO user_books(created_at, user_id, book_id, read, start_read_date, end_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Les dieux ont soif'), true,'2026-01-30 00:00:00+00:00','2026-02-13 00:00:00+00:00',7);
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg')); INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg'));

View File

@@ -1,6 +1,6 @@
{ {
"name": "bibliomane", "name": "bibliomane",
"version": "0.3.0", "version": "0.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {

View File

@@ -47,13 +47,8 @@ onMounted(() => {
<template> <template>
<nav class="navbar"> <nav class="navbar">
<div class="navbar-brand"> <div class="navbar-brand">
<RouterLink <RouterLink to="/" class="navbar-item" :title="'bibliomane v' + appVersion">
to="/" <img class="ml-3" src="/image/logo.svg" />
class="navbar-item"
:title="'bibliomane v' + appVersion"
activeClass="is-active"
>
<img src="/image/logo.svg" />
</RouterLink> </RouterLink>
<div class="navbar-item is-hidden-desktop"> <div class="navbar-item is-hidden-desktop">
<a <a

View File

@@ -3,9 +3,7 @@ import { ref, computed } from 'vue'
import { import {
getBook, getBook,
getImagePathOrDefault, getImagePathOrDefault,
putReadBook, putUpdateBook,
putWantReadBook,
putRateBook,
putStartReadDate, putStartReadDate,
putStartReadDateUnset, putStartReadDateUnset,
putEndReadDate, putEndReadDate,
@@ -15,6 +13,7 @@ import {
import { useRouter, onBeforeRouteUpdate } from 'vue-router' import { useRouter, onBeforeRouteUpdate } from 'vue-router'
import { VRating } from 'vuetify/components/VRating' import { VRating } from 'vuetify/components/VRating'
import BookFormIcons from './BookFormIcons.vue' import BookFormIcons from './BookFormIcons.vue'
import ReviewWidget from './ReviewWidget.vue'
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
@@ -37,7 +36,12 @@ function onRatingUpdate(rating) {
data.value.read = true data.value.read = true
data.value.wantread = false data.value.wantread = false
} }
putRateBook(props.id, { rating: data.value.rating }) putUpdateBook(props.id, { rating: data.value.rating })
}
function onReviewUpdate(review) {
data.value.review = review
putUpdateBook(props.id, { review: data.value.review })
} }
async function onReadIconClick() { async function onReadIconClick() {
@@ -53,7 +57,7 @@ async function onReadIconClick() {
function onWantReadIconClick() { function onWantReadIconClick() {
data.value.wantread = !data.value.wantread data.value.wantread = !data.value.wantread
putWantReadBook(props.id, { wantread: data.value.wantread }) putUpdateBook(props.id, { wantread: data.value.wantread })
} }
async function onStartReadIconClick() { async function onStartReadIconClick() {
@@ -100,17 +104,6 @@ function goToAuthor() {
<figure class="image"> <figure class="image">
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" /> <img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
</figure> </figure>
<VRating
half-increments
hover
:length="5"
size="x-large"
density="compact"
:model-value="data.rating / 2"
@update:modelValue="onRatingUpdate"
active-color="bulma-body-color"
class="centered"
/>
</div> </div>
<div class="column"> <div class="column">
<h3 class="title">{{ data.title }}</h3> <h3 class="title">{{ data.title }}</h3>
@@ -119,6 +112,12 @@ function goToAuthor() {
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div> <div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div> <div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div> <div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
<ReviewWidget
:reviewtext="data.review"
:rating="data.rating"
@on-review-update="onReviewUpdate"
@on-rating-update="onRatingUpdate"
/>
</div> </div>
<div class="column"> <div class="column">
<BookFormIcons <BookFormIcons
@@ -141,12 +140,6 @@ img {
width: auto; width: auto;
} }
.centered {
display: flex;
justify-content: center;
align-items: center;
}
@media (min-width: 1024px) { @media (min-width: 1024px) {
.left-panel { .left-panel {
margin-left: 3rem; margin-left: 3rem;

View File

@@ -59,7 +59,7 @@ async function onStartReadIconClick() {
> >
<BigIcon <BigIcon
icon="BIconBook" icon="BIconBook"
:legend="$t('bookform.wantread')" :legend="$t('bookform.startread')"
:is-set="isStartReadExpanded()" :is-set="isStartReadExpanded()"
@click="onStartReadIconClick" @click="onStartReadIconClick"
/> />

View File

@@ -39,4 +39,13 @@ const today = new Date().toISOString().slice(0, 10)
font-size: 26px; font-size: 26px;
border-radius: 5px; border-radius: 5px;
} }
@media (max-width: 1024px) {
.datelabel {
font-size: 18px;
}
.datepicker {
font-size: 18px;
}
}
</style> </style>

134
front/src/ReviewWidget.vue Normal file
View File

@@ -0,0 +1,134 @@
<script setup>
import { ref } from 'vue'
import { VRating } from 'vuetify/components/VRating'
const props = defineProps({
rating: Number,
reviewtext: String,
})
const isTextareaExpanded = ref(false)
const isTextareaTransitionEnabled = ref(true)
defineEmits('onRatingUpdate', 'onReviewUpdate')
function computeTextareaClass() {
let classAttr =
isTextareaExpanded && isTextareaExpanded.value ? 'textarea-expanded' : 'textarea-normal'
if (isTextareaTransitionEnabled && isTextareaTransitionEnabled.value) {
classAttr += ' transition-height'
}
return classAttr
}
function onTextAreaFocus() {
isTextareaExpanded.value = true
setTimeout(() => {
isTextareaTransitionEnabled.value = false
}, 500)
}
</script>
<template>
<div class="maincontainer py-5">
<div class="widget-header mb-5 full-width">
<div class="widget-title ml-3">
<h2>{{ $t('review.title') }}</h2>
<span class="ml-3">
<b-icon-pen />
</span>
</div>
<VRating
half-increments
hover
:length="5"
size="x-large"
density="compact"
:model-value="rating / 2"
@update:modelValue="(r) => $emit('onRatingUpdate', r)"
active-color="bulma-body-color"
class="widget-rating centered"
/>
</div>
<div class="full-width centered">
<textarea
:placeholder="$t('review.textplaceholder')"
class="widget-textarea mx-4"
@change="(e) => $emit('onReviewUpdate', e.target.value)"
@focus="onTextAreaFocus"
:class="computeTextareaClass()"
>{{ reviewtext }}</textarea
>
</div>
</div>
</template>
<style scoped>
.maincontainer {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
border: solid;
border-radius: 20px;
padding: 10px;
}
.widget-header {
display: flex;
}
.widget-title {
flex: 2;
font-size: 2em;
display: flex;
justify-content: center;
align-items: center;
}
.widget-title h2 {
font-weight: bold;
}
.widget-rating {
flex: 2;
}
.widget-textarea {
color: var(--bulma-body-color);
background-color: var(--bulma-text-20);
width: 95%;
border-radius: 30px;
border: none;
padding: 15px;
}
.textarea-normal {
height: 80px;
resize: none;
}
.textarea-expanded {
height: 350px;
resize: vertical;
}
.transition-height {
transition: height 0.5s;
}
.full-width {
width: 100%;
}
@media (max-width: 1024px) {
.widget-header {
display: flex;
flex-wrap: wrap;
}
.widget-title {
font-size: 1.5em;
width: 100%;
margin-bottom: 10px;
}
}
</style>

View File

@@ -99,35 +99,31 @@ export async function postImportBook(id, language) {
} }
export async function putReadBook(bookId) { export async function putReadBook(bookId) {
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { read: true }, 'PUT')
} }
export async function putUnreadBook(bookId) { export async function putUnreadBook(bookId) {
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: false }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { read: false }, 'PUT')
} }
export async function putEndReadDate(bookId, enddate) { export async function putEndReadDate(bookId, enddate) {
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: enddate }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: enddate }, 'PUT')
} }
export async function putEndReadDateUnset(bookId) { export async function putEndReadDateUnset(bookId) {
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: 'null' }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: 'null' }, 'PUT')
} }
export async function putStartReadDateUnset(bookId) { export async function putStartReadDateUnset(bookId) {
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: 'null' }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { startDate: 'null' }, 'PUT')
} }
export async function putStartReadDate(bookId, startdate) { export async function putStartReadDate(bookId, startdate) {
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: startdate }, 'PUT') return genericPayloadCall('/ws/book/' + bookId, { startDate: startdate }, 'PUT')
} }
export async function putWantReadBook(bookId, payload) { export async function putUpdateBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId + '/wantread', payload, 'PUT') return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT')
}
export async function putRateBook(bookId, payload) {
return genericPayloadCall('/ws/book/' + bookId + '/rate', payload, 'PUT')
} }
export function postLogin(user) { export function postLogin(user) {

View File

@@ -59,6 +59,7 @@
}, },
"bookform": { "bookform": {
"error": "Error when loading book: {error}", "error": "Error when loading book: {error}",
"reviewbtn": "My review",
"read": "Read", "read": "Read",
"startread": "Started", "startread": "Started",
"wantread": "Interested" "wantread": "Interested"
@@ -80,5 +81,9 @@
"releasedate": "Release date:", "releasedate": "Release date:",
"publisher": "Publisher:", "publisher": "Publisher:",
"importing": "Importing..." "importing": "Importing..."
},
"review": {
"title": "My review",
"textplaceholder": "Write my review..."
} }
} }

View File

@@ -59,6 +59,7 @@
}, },
"bookform": { "bookform": {
"error": "Erreur pendant le chargement du livre: {error}", "error": "Erreur pendant le chargement du livre: {error}",
"reviewbtn": "Ma critique",
"read": "Lu", "read": "Lu",
"startread": "Commencé", "startread": "Commencé",
"wantread": "À lire" "wantread": "À lire"
@@ -80,5 +81,9 @@
"releasedate": "Date de publication : ", "releasedate": "Date de publication : ",
"publisher": "Maison d'édition : ", "publisher": "Maison d'édition : ",
"importing": "Import en cours..." "importing": "Import en cours..."
},
"review": {
"title": "Ma critique",
"textplaceholder": "Écrire ma critique..."
} }
} }

View File

@@ -1,3 +1,9 @@
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }
.centered {
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -22,6 +22,7 @@ func TestGetBook_Ok(t *testing.T) {
Rating: 10, Rating: 10,
Read: true, Read: true,
CoverPath: "/static/bookcover/dunchateaulautre.jpg", CoverPath: "/static/bookcover/dunchateaulautre.jpg",
Review: "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
}, book) }, book)
} }

View File

@@ -1,87 +0,0 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
payload :=
`{
"rating": 5
}`
bookId := "17"
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 5, book.Rating)
assert.Equal(t, true, book.Read)
}
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
payload :=
`{
"rating": 7
}`
bookId := "18"
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 7, book.Rating)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
payload :=
`{
"rating": 6
}`
bookId := "2"
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 6, book.Rating)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
payload :=
`{
"rating": "bad"
}`
bookId := "18"
testPutRateUserBooks(t, payload, bookId, http.StatusInternalServerError)
}
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
payload :=
`{
"rating": -3
}`
bookId := "18"
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
}
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
payload :=
`{
"rating": 15
}`
bookId := "18"
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
}
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
payload :=
`{
"rating": 15
}`
bookId := "18574"
testPutRateUserBooks(t, payload, bookId, http.StatusNotFound)
}
func testPutRateUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/rate")
}

View File

@@ -1,63 +0,0 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
payload :=
`{
"read": true
}`
bookId := "21"
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
payload :=
`{
"read": true,
"endDate": "2025-10-20"
}`
bookId := "9"
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, "2025-10-20", book.EndReadDate)
}
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
payload :=
`{
"read": true,
"endDate": "null"
}`
bookId := "9"
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, "", book.EndReadDate)
}
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
payload :=
`{
"read": false
}`
bookId := "9"
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, false, book.Read)
assert.Equal(t, "", book.EndReadDate)
}
func testPutReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/read")
}

View File

@@ -1,53 +0,0 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPutStartReadUserBooks_NoDate(t *testing.T) {
payload :=
`{
"date": "2025-11-19"
}`
bookId := "6"
testPutStartReadUserBooks(t, payload, bookId, http.StatusBadRequest)
}
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
payload :=
`{
"startDate": "19/11/2025"
}`
bookId := "6"
testPutStartReadUserBooks(t, payload, bookId, http.StatusInternalServerError)
}
func TestPutStartReadUserBooks_NewReadOk(t *testing.T) {
payload :=
`{
"startDate": "2025-11-19"
}`
bookId := "6"
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, "2025-11-19", book.StartReadDate)
}
func TestPutStartReadUserBooks_Unset(t *testing.T) {
payload :=
`{
"startDate": "null"
}`
bookId := "6"
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, "", book.StartReadDate)
}
func testPutStartReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/startread")
}

View File

@@ -0,0 +1,190 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
payload :=
`{
"rating": 5
}`
bookId := "17"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 5, book.Rating)
assert.Equal(t, true, book.Read)
}
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
payload :=
`{
"rating": 7
}`
bookId := "18"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 7, book.Rating)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
payload :=
`{
"rating": 6
}`
bookId := "2"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, 6, book.Rating)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
payload :=
`{
"rating": "bad"
}`
bookId := "18"
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
}
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
payload :=
`{
"rating": -3
}`
bookId := "18"
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
}
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
payload :=
`{
"rating": 15
}`
bookId := "18"
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
}
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
payload :=
`{
"rating": 15
}`
bookId := "18574"
testPutUserBooks(t, payload, bookId, http.StatusNotFound)
}
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
payload :=
`{
"read": true
}`
bookId := "21"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, false, book.WantRead)
}
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
payload :=
`{
"read": true,
"endDate": "2025-10-20"
}`
bookId := "9"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, "2025-10-20", book.EndReadDate)
}
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
payload :=
`{
"read": true,
"endDate": "null"
}`
bookId := "9"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.Read)
assert.Equal(t, "", book.EndReadDate)
}
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
payload :=
`{
"read": false
}`
bookId := "9"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, false, book.Read)
assert.Equal(t, "", book.EndReadDate)
}
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
payload :=
`{
"startDate": "19/11/2025"
}`
bookId := "6"
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
}
func TestPutStartReadUserBooks_NewReadOk(t *testing.T) {
payload :=
`{
"startDate": "2025-11-19"
}`
bookId := "6"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, "2025-11-19", book.StartReadDate)
}
func TestPutStartReadUserBooks_Unset(t *testing.T) {
payload :=
`{
"startDate": "null"
}`
bookId := "6"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, "", book.StartReadDate)
}
func TestPutWantRead_SetTrue(t *testing.T) {
payload :=
`{
"wantread": true
}`
bookId := "17"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.WantRead)
}
func TestPutWantRead_SetFalse(t *testing.T) {
payload :=
`{
"wantread": false
}`
bookId := "2"
testPutUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, false, book.WantRead)
}
func testPutUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId)
}

View File

@@ -1,35 +0,0 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPutWantRead_SetTrue(t *testing.T) {
payload :=
`{
"wantread": true
}`
bookId := "17"
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, true, book.WantRead)
}
func TestPutWantRead_SetFalse(t *testing.T) {
payload :=
`{
"wantread": false
}`
bookId := "2"
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
book := testGetBook(t, bookId, http.StatusOK)
assert.Equal(t, false, book.WantRead)
}
func testPutWantReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/wantread")
}

View File

@@ -21,6 +21,15 @@ type BookPostImport struct {
Lang string `json:"lang" binding:"required,max=5"` Lang string `json:"lang" binding:"required,max=5"`
} }
type UserBookPutUpdate struct {
Read *bool `json:"read"`
EndDate *string `json:"endDate"`
WantRead *bool `json:"wantread"`
Rating *int `json:"rating"`
StartDate *string `json:"startDate"`
Review *string `json:"review"`
}
type FileInfoPost struct { type FileInfoPost struct {
FileID uint `json:"fileId"` FileID uint `json:"fileId"`
FilePath string `json:"filepath"` FilePath string `json:"filepath"`

View File

@@ -14,6 +14,7 @@ type BookGet struct {
InventaireId string `json:"inventaireid"` InventaireId string `json:"inventaireid"`
OpenLibraryId string `json:"openlibraryid"` OpenLibraryId string `json:"openlibraryid"`
Summary string `json:"summary"` Summary string `json:"summary"`
Review string `json:"review"`
Rating int `json:"rating"` Rating int `json:"rating"`
Read bool `json:"read"` Read bool `json:"read"`
WantRead bool `json:"wantread"` WantRead bool `json:"wantread"`

View File

@@ -132,6 +132,27 @@ func TestCallInventaireEdition(t *testing.T) {
result) result)
} }
func TestCalInventaireEditionNoAuthor(t *testing.T) {
result, err := CallInventaireEdition(getBaseInventaireUrl(), "isbn:9782226487162", "fr")
if err != nil {
t.Error(err)
}
assert.Equal(t,
InventaireEditionDetailedSingleResult{
Id: "isbn:9782226487162",
Title: "Les Yeux de Mona",
Author: nil,
Description: "",
ISBN: "978-2-226-48716-2",
Publisher: "éditions Albin Michel",
ReleaseDate: "2024-02-01",
Image: "https://inventaire.io/img/entities/3ca857913983d694be03dee712bb2af9e2c51747",
Lang: "fr",
},
result)
}
func TestCallInventaireEditionFromISBN(t *testing.T) { func TestCallInventaireEditionFromISBN(t *testing.T) {
result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr") result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr")
if err != nil { if err != nil {

View File

@@ -2,6 +2,7 @@ package inventaire
import ( import (
"math" "math"
"slices"
"sort" "sort"
"git.artlef.fr/bibliomane/internal/callapiutils" "git.artlef.fr/bibliomane/internal/callapiutils"
@@ -32,14 +33,15 @@ func CallInventaireEditionFromWork(inventaireUrl string, workId string, lang str
if err != nil { if err != nil {
return queryResult, err return queryResult, err
} }
queryResult.Count = int64(len(uris.Uris))
sort.Strings(uris.Uris) sort.Strings(uris.Uris)
limitedUris := uris.Uris listUris := slices.Compact(uris.Uris)
queryResult.Count = int64(len(listUris))
limitedUris := listUris
if limit != 0 { if limit != 0 {
l := len(uris.Uris) l := len(listUris)
startIndex := int(math.Min(float64(offset), float64(l))) startIndex := int(math.Min(float64(offset), float64(l)))
endIndex := int(math.Min(float64(limit+offset), float64(l))) endIndex := int(math.Min(float64(limit+offset), float64(l)))
limitedUris = uris.Uris[startIndex:endIndex] limitedUris = listUris[startIndex:endIndex]
} }
editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris) editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris)

View File

@@ -15,6 +15,7 @@ type UserBook struct {
Rating int Rating int
Read bool Read bool
WantRead bool WantRead bool
Review string
StartReadDate *time.Time StartReadDate *time.Time
EndReadDate *time.Time EndReadDate *time.Time
} }

View File

@@ -79,6 +79,8 @@ func computeValidationMessage(ac *appcontext.AppContext, fe *validator.FieldErro
return i18nresource.GetTranslatedMessage(ac, "ValidationRequired") return i18nresource.GetTranslatedMessage(ac, "ValidationRequired")
case "min": case "min":
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param()) return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param())
case "gte":
return fmt.Sprintf("Should be greater than %s", (*fe).Param())
case "max": case "max":
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param()) return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param())
default: default:

View File

@@ -11,7 +11,7 @@ func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.BookGet, error)
var book dto.BookGet var book dto.BookGet
query := db.Model(&model.Book{}) query := db.Model(&model.Book{})
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " + selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " +
"user_books.rating, user_books.read, user_books.want_read, " + "user_books.review, user_books.rating, user_books.read, user_books.want_read, " +
"DATE(user_books.start_read_date) as start_read_date, " + "DATE(user_books.start_read_date) as start_read_date, " +
"DATE(user_books.end_read_date) AS end_read_date, " + "DATE(user_books.end_read_date) AS end_read_date, " +
selectStaticFilesPath() selectStaticFilesPath()

View File

@@ -40,18 +40,21 @@ func PostImportBookHandler(ac appcontext.AppContext) {
} }
func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) { func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) {
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
if err != nil {
return nil, err
}
book := model.Book{ book := model.Book{
Title: inventaireEdition.Title, Title: inventaireEdition.Title,
SmallDescription: inventaireEdition.Description, SmallDescription: inventaireEdition.Description,
InventaireID: inventaireEdition.Id, InventaireID: inventaireEdition.Id,
Author: *author,
AddedBy: *user, AddedBy: *user,
} }
if inventaireEdition.Author != nil {
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
if err != nil {
return nil, err
}
book.Author = *author
}
if inventaireEdition.Image != "" { if inventaireEdition.Image != "" {
cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image) cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image)
if err != nil { if err != nil {
@@ -59,7 +62,7 @@ func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventai
} }
book.Cover = cover book.Cover = cover
} }
err = ac.Db.Save(&book).Error err := ac.Db.Save(&book).Error
return &book, err return &book, err
} }

View File

@@ -7,21 +7,35 @@ import (
"time" "time"
"git.artlef.fr/bibliomane/internal/appcontext" "git.artlef.fr/bibliomane/internal/appcontext"
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/model" "git.artlef.fr/bibliomane/internal/model"
"git.artlef.fr/bibliomane/internal/myvalidator" "git.artlef.fr/bibliomane/internal/myvalidator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"gorm.io/gorm" "gorm.io/gorm"
) )
func PutReadUserBookHandler(ac appcontext.AppContext) { func PutUserBookHandler(ac appcontext.AppContext) {
data, err := retrieveDataFromContext(ac) bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
bookId := uint(bookId64)
if err != nil { if err != nil {
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
return return
} }
bookId := data.BookId err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
user := data.User if err != nil {
var read userbookPutRead myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
err = ac.C.ShouldBindJSON(&read) return
}
user, err := ac.GetAuthenticatedUser()
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
var userBookPut dto.UserBookPutUpdate
err = ac.C.ShouldBindJSON(&userBookPut)
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
@@ -32,14 +46,56 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
return return
} }
userbook.Read = read.Read if userBookPut.Read != nil {
err = updateReadStatus(&userbook, &userBookPut)
if read.EndDate != "" {
d, err := parseDate(read.EndDate)
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
}
if userBookPut.WantRead != nil {
userbook.WantRead = *userBookPut.WantRead
}
if userBookPut.StartDate != nil {
d, err := parseDate(*userBookPut.StartDate)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook.StartReadDate = d
}
if userBookPut.Rating != nil {
err = validateRating(*userBookPut.Rating)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
}
updateRating(&userbook, &userBookPut)
}
if userBookPut.Review != nil {
userbook.Review = *userBookPut.Review
}
ac.Db.Save(&userbook)
ac.C.String(http.StatusOK, "Success")
}
func validateRating(rating int) error {
//struct used for validation
var ratingStruct struct {
Rating int `validate:"gte=0,lte=10"`
}
ratingStruct.Rating = rating
validate := validator.New()
return validate.Struct(ratingStruct)
}
func updateReadStatus(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) error {
userbook.Read = *userBookPut.Read
if userBookPut.EndDate != nil && *userBookPut.EndDate != "" {
d, err := parseDate(*userBookPut.EndDate)
if err != nil {
return err
}
userbook.EndReadDate = d userbook.EndReadDate = d
} }
@@ -52,84 +108,11 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
if !userbook.Read { if !userbook.Read {
userbook.EndReadDate = nil userbook.EndReadDate = nil
} }
return nil
ac.Db.Save(&userbook)
ac.C.String(http.StatusOK, "Success")
} }
func PutWantReadUserBookHandler(ac appcontext.AppContext) { func updateRating(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) {
data, err := retrieveDataFromContext(ac) userbook.Rating = *userBookPut.Rating
if err != nil {
return
}
bookId := data.BookId
user := data.User
var wantread userbookPutWantRead
err = ac.C.ShouldBindJSON(&wantread)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook.WantRead = wantread.WantRead
ac.Db.Save(&userbook)
ac.C.String(http.StatusOK, "Success")
}
func PutStartReadUserBookHandler(ac appcontext.AppContext) {
data, err := retrieveDataFromContext(ac)
if err != nil {
return
}
bookId := data.BookId
user := data.User
var startDateToParse userbookPutStartRead
err = ac.C.ShouldBindJSON(&startDateToParse)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
d, err := parseDate(startDateToParse.StartDate)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook.StartReadDate = d
ac.Db.Save(&userbook)
ac.C.String(http.StatusOK, "Success")
}
func PutRateUserBookHandler(ac appcontext.AppContext) {
data, err := retrieveDataFromContext(ac)
if err != nil {
return
}
bookId := data.BookId
user := data.User
var rating userbookPutRating
err = ac.C.ShouldBindJSON(&rating)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
userbook.Rating = rating.Rating
//if rated, set to "read" (a rating = 0 means unrated) //if rated, set to "read" (a rating = 0 means unrated)
if userbook.Rating > 0 { if userbook.Rating > 0 {
@@ -137,30 +120,6 @@ func PutRateUserBookHandler(ac appcontext.AppContext) {
//if set to read, remove want read //if set to read, remove want read
userbook.WantRead = false userbook.WantRead = false
} }
ac.Db.Save(&userbook)
ac.C.String(http.StatusOK, "Success")
}
type userbookPutRead struct {
Read bool `json:"read"`
EndDate string `json:"endDate"`
}
type userbookPutWantRead struct {
WantRead bool `json:"wantread"`
}
type userbookPutRating struct {
Rating int `json:"rating" binding:"min=0,max=10"`
}
type userbookPutStartRead struct {
StartDate string `json:"startDate" binding:"required"`
}
type apiCallData struct {
BookId uint
User model.User
} }
func parseDate(dateToParse string) (*time.Time, error) { func parseDate(dateToParse string) (*time.Time, error) {
@@ -173,27 +132,6 @@ func parseDate(dateToParse string) (*time.Time, error) {
} }
} }
func retrieveDataFromContext(ac appcontext.AppContext) (apiCallData, error) {
bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
bookId := uint(bookId64)
if err != nil {
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
return apiCallData{}, err
}
err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return apiCallData{}, err
}
user, fetchUserErr := ac.GetAuthenticatedUser()
if fetchUserErr != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return apiCallData{}, fetchUserErr
}
return apiCallData{BookId: bookId, User: user}, nil
}
func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) { func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) {
var userbook model.UserBook var userbook model.UserBook
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook) res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook)

View File

@@ -58,17 +58,8 @@ func Setup(config *config.Config) *gin.Engine {
ws.GET("/book/:id", func(c *gin.Context) { ws.GET("/book/:id", func(c *gin.Context) {
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
}) })
ws.PUT("/book/:id/read", func(c *gin.Context) { ws.PUT("/book/:id", func(c *gin.Context) {
routes.PutReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) routes.PutUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
ws.PUT("/book/:id/wantread", func(c *gin.Context) {
routes.PutWantReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
ws.PUT("/book/:id/startread", func(c *gin.Context) {
routes.PutStartReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
ws.PUT("/book/:id/rate", func(c *gin.Context) {
routes.PutRateUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
}) })
ws.POST("/book", func(c *gin.Context) { ws.POST("/book", func(c *gin.Context) {
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})

View File

@@ -12,7 +12,6 @@ import (
"git.artlef.fr/bibliomane/internal/config" "git.artlef.fr/bibliomane/internal/config"
"git.artlef.fr/bibliomane/internal/setup" "git.artlef.fr/bibliomane/internal/setup"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
) )
func TestSetup() *gin.Engine { func TestSetup() *gin.Engine {
@@ -62,5 +61,7 @@ func TestBookPutCallWithDemoPayload(t *testing.T, payload string, bookId string,
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, expectedCode, w.Code) if w.Code != expectedCode {
t.Errorf("%s", w.Body.String())
}
} }

View File

@@ -6,7 +6,7 @@ import (
) )
func main() { func main() {
applicationVersion := "0.3.0" applicationVersion := "0.4.0"
c := config.LoadConfig(applicationVersion) c := config.LoadConfig(applicationVersion)
r := setup.Setup(&c) r := setup.Setup(&c)
r.Run(":" + c.Port) r.Run(":" + c.Port)