├── .env ├── .gitignore ├── .travis.yml ├── Dockerfile.in ├── LICENSE ├── Makefile ├── README.md ├── build ├── build.sh ├── test.sh └── test_ci.sh ├── cmd └── blog_backend │ ├── apis │ ├── apis_test.go │ ├── book.go │ ├── book_test.go │ ├── email.go │ ├── post.go │ ├── post_test.go │ ├── project.go │ ├── project_test.go │ ├── section.go │ ├── section_test.go │ ├── tag.go │ └── tag_test.go │ ├── config │ └── config.go │ ├── daos │ ├── book.go │ ├── book_test.go │ ├── post.go │ ├── post_test.go │ ├── project.go │ ├── project_test.go │ ├── section.go │ ├── section_test.go │ ├── tag.go │ └── tag_test.go │ ├── main.go │ ├── middleware │ ├── brotli_test.go │ ├── cors.go │ └── cors_test.go │ ├── models │ └── model.go │ ├── services │ ├── book.go │ ├── book_test.go │ ├── post.go │ ├── post_test.go │ ├── project.go │ ├── project_test.go │ ├── section.go │ ├── section_test.go │ ├── tag.go │ └── tag_test.go │ └── test_data │ ├── db.sql │ ├── init.go │ └── test_case_data │ ├── book_t1.json │ ├── post_t1.json │ ├── post_t3.json │ ├── project_t1.json │ ├── section_t1.json │ └── tag_t1.json ├── config ├── errors.yaml └── server.yaml ├── docker-compose.yml ├── go.mod ├── pkg └── version.go ├── postgres ├── Dockerfile ├── README.md ├── create_db.sh ├── schema.sql └── test_data.sql ├── reports.sh └── sonar-project.properties /.env: -------------------------------------------------------------------------------- 1 | POPULATE_DB=1 2 | HOST=localhost 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | bin 12 | .go 13 | .push-* 14 | .container-* 15 | .dockerfile-* 16 | 17 | go.sum 18 | 19 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: go 4 | sudo: required 5 | services: 6 | - docker 7 | script: 8 | - export GO111MODULE=on 9 | - go mod vendor 10 | - make build 11 | - test -f bin/linux_amd64/blog_backend 12 | - make all-container 13 | - docker images | grep "^martinheinz/blog_backend.*__linux_amd64" 14 | - make test 15 | 16 | - language: go 17 | addons: 18 | sonarcloud: 19 | organization: martinheinz-github 20 | token: 21 | secure: "hbGHoYneczpzXmgBLoSKDNEXH7Gv2FVI7LGyHGXdUq3qdaJfRPzDcnMneBNwOl9/5tYYtbtx5uL9xuMa7VFzvosFhiszb8zEpPEAhUQ7jUYS/6Zchvsb5GgGhb8kk2Zud4UpoRbNr8iaqU6LCbxjMGx/SJkvyqMsMjX5zudMuV3OwRlX7JbsLrENdcb6S0PJAMknNRNEZoVjJ8uuJ0MPFb8fns19CnkiBylY64fUPzdiEh7CRSaxI5CbsNxTRZl4gjEPf3orlnHrMG/HhxOZ2PcCaLpW7+aZjQUd0RTYyOmX4u330tzQyCmTCFlG3Kl/XcDSGGBIGYcTzMAStfZ84NByV3e3jKEXesaXSfsCGZZqFbOtqWAEA60bBRyZFjkNIUUH1+uH3EcKaiQNPdbn46M7gnW4nwHlI7gpkCL2oDMJpHh7DcBDDc0ZtzSrN7lUzYO6M+i+8Xf4O/xnwmNJH0NHXZ9dCnQCfzlLLoFtXkflmMpu7apLiLKD6fdcQbatbu189sAiDJwxx4ehTI+zKCYfyf+exu/T8DaZtzctNh4fZ1NbXkhBFPlVf+++PXXiAWlK7nN6lhe15LAS50ZSmP2rjjScQKoh6F1WrROreguWNZdOlG58FH2Cxno7xb3h0yoGVRfA6hhlw5GQv62Lk3ykzzzfSWywEkHt6nnNPsc=" 22 | before_script: 23 | - ./reports.sh 24 | - export GO111MODULE=on 25 | - go mod vendor 26 | - make ci 27 | script: 28 | - sonar-scanner 29 | 30 | - language: go 31 | before_script: 32 | - ./reports.sh 33 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 34 | - chmod +x ./cc-test-reporter 35 | - ./cc-test-reporter before-build 36 | 37 | script: 38 | - export GO111MODULE=on 39 | - go mod vendor 40 | - make ci 41 | 42 | after_script: 43 | - ./cc-test-reporter after-build -t gocov --exit-code $TRAVIS_TEST_RESULT 44 | 45 | - language: go 46 | services: 47 | - docker 48 | if: branch = master 49 | script: 50 | - export GO111MODULE=on 51 | - go mod vendor 52 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 53 | - make container 54 | - make push 55 | 56 | notifications: 57 | email: false 58 | -------------------------------------------------------------------------------- /Dockerfile.in: -------------------------------------------------------------------------------- 1 | FROM {ARG_FROM} 2 | 3 | ADD bin/{ARG_OS}_{ARG_ARCH}/{ARG_BIN} /{ARG_BIN} 4 | COPY ./config /config 5 | 6 | EXPOSE 8080 7 | 8 | ENTRYPOINT ["/{ARG_BIN}"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MartinHeinz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The binary to build (just the basename). 2 | BIN := blog_backend 3 | 4 | # Where to push the docker image. 5 | REGISTRY ?= martinheinz 6 | 7 | # This version-strategy uses git tags to set the version string 8 | VERSION := $(shell git describe --tags --always --dirty) 9 | # 10 | # This version-strategy uses a manual value to set the version string 11 | #VERSION := 1.2.3 12 | 13 | ### 14 | ### These variables should not need tweaking. 15 | ### 16 | 17 | SRC_DIRS := cmd pkg # directories which hold app source (not vendored) 18 | 19 | ALL_PLATFORMS := linux/amd64 20 | 21 | # Used internally. Users should pass GOOS and/or GOARCH. 22 | OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) 23 | ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) 24 | 25 | BASEIMAGE ?= gcr.io/distroless/static 26 | 27 | IMAGE := $(REGISTRY)/$(BIN) 28 | TAG := $(VERSION)__$(OS)_$(ARCH) 29 | 30 | BUILD_IMAGE ?= golang:1.12-alpine 31 | TEST_IMAGE ?= martinheinz/golang:1.12-alpine-test 32 | 33 | # If you want to build all binaries, see the 'all-build' rule. 34 | # If you want to build all containers, see the 'all-container' rule. 35 | # If you want to build AND push all containers, see the 'all-push' rule. 36 | all: build 37 | 38 | # For the following OS/ARCH expansions, we transform OS/ARCH into OS_ARCH 39 | # because make pattern rules don't match with embedded '/' characters. 40 | 41 | build-%: 42 | @$(MAKE) build \ 43 | --no-print-directory \ 44 | GOOS=$(firstword $(subst _, ,$*)) \ 45 | GOARCH=$(lastword $(subst _, ,$*)) 46 | 47 | container-%: 48 | @$(MAKE) container \ 49 | --no-print-directory \ 50 | GOOS=$(firstword $(subst _, ,$*)) \ 51 | GOARCH=$(lastword $(subst _, ,$*)) 52 | 53 | push-%: 54 | @$(MAKE) push \ 55 | --no-print-directory \ 56 | GOOS=$(firstword $(subst _, ,$*)) \ 57 | GOARCH=$(lastword $(subst _, ,$*)) 58 | 59 | all-build: $(addprefix build-, $(subst /,_, $(ALL_PLATFORMS))) 60 | 61 | all-container: $(addprefix container-, $(subst /,_, $(ALL_PLATFORMS))) 62 | 63 | all-push: $(addprefix push-, $(subst /,_, $(ALL_PLATFORMS))) 64 | 65 | build: bin/$(OS)_$(ARCH)/$(BIN) 66 | 67 | # Directories that we need created to build/test. 68 | BUILD_DIRS := bin/$(OS)_$(ARCH) \ 69 | .go/bin/$(OS)_$(ARCH) \ 70 | .go/cache 71 | 72 | # The following structure defeats Go's (intentional) behavior to always touch 73 | # result files, even if they have not changed. This will still run `go` but 74 | # will not trigger further work if nothing has actually changed. 75 | OUTBIN = bin/$(OS)_$(ARCH)/$(BIN) 76 | $(OUTBIN): .go/$(OUTBIN).stamp 77 | @true 78 | 79 | # This will build the binary under ./.go and update the real binary iff needed. 80 | .PHONY: .go/$(OUTBIN).stamp 81 | .go/$(OUTBIN).stamp: $(BUILD_DIRS) 82 | @echo "making $(OUTBIN)" 83 | @docker run \ 84 | -i \ 85 | --rm \ 86 | -u $$(id -u):$$(id -g) \ 87 | -v $$(pwd):/src \ 88 | -w /src \ 89 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ 90 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ 91 | -v $$(pwd)/.go/cache:/.cache \ 92 | --env HTTP_PROXY=$(HTTP_PROXY) \ 93 | --env HTTPS_PROXY=$(HTTPS_PROXY) \ 94 | $(BUILD_IMAGE) \ 95 | /bin/sh -c " \ 96 | ARCH=$(ARCH) \ 97 | OS=$(OS) \ 98 | VERSION=$(VERSION) \ 99 | ./build/build.sh \ 100 | " 101 | @if ! cmp -s .go/$(OUTBIN) $(OUTBIN); then \ 102 | mv .go/$(OUTBIN) $(OUTBIN); \ 103 | date >$@; \ 104 | fi 105 | 106 | # Example: make shell CMD="-c 'date > datefile'" 107 | shell: $(BUILD_DIRS) 108 | @echo "launching a shell in the containerized build environment" 109 | @docker run \ 110 | -ti \ 111 | --rm \ 112 | -u $$(id -u):$$(id -g) \ 113 | -v $$(pwd):/src \ 114 | -w /src \ 115 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ 116 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ 117 | -v $$(pwd)/.go/cache:/.cache \ 118 | --env HTTP_PROXY=$(HTTP_PROXY) \ 119 | --env HTTPS_PROXY=$(HTTPS_PROXY) \ 120 | $(BUILD_IMAGE) \ 121 | /bin/sh $(CMD) 122 | 123 | # Used to track state in hidden files. 124 | DOTFILE_IMAGE = $(subst /,_,$(IMAGE))-$(TAG) 125 | 126 | container: .container-$(DOTFILE_IMAGE) say_container_name 127 | .container-$(DOTFILE_IMAGE): bin/$(OS)_$(ARCH)/$(BIN) Dockerfile.in 128 | @sed \ 129 | -e 's|{ARG_BIN}|$(BIN)|g' \ 130 | -e 's|{ARG_ARCH}|$(ARCH)|g' \ 131 | -e 's|{ARG_OS}|$(OS)|g' \ 132 | -e 's|{ARG_FROM}|$(BASEIMAGE)|g' \ 133 | Dockerfile.in > .dockerfile-$(OS)_$(ARCH) 134 | @docker build -t $(IMAGE):$(TAG) -t $(IMAGE):latest -f .dockerfile-$(OS)_$(ARCH) . 135 | @docker images -q $(IMAGE):$(TAG) > $@ 136 | 137 | say_container_name: 138 | @echo "container: $(IMAGE):$(TAG)" 139 | @echo "container: $(IMAGE):latest" 140 | 141 | push: .push-$(DOTFILE_IMAGE) say_push_name 142 | .push-$(DOTFILE_IMAGE): .container-$(DOTFILE_IMAGE) 143 | @docker push $(IMAGE):$(TAG) 144 | @docker push $(IMAGE):latest 145 | 146 | say_push_name: 147 | @echo "pushed: $(IMAGE):$(TAG)" 148 | @echo "pushed: $(IMAGE):latest" 149 | 150 | manifest-list: push 151 | platforms=$$(echo $(ALL_PLATFORMS) | sed 's/ /,/g'); \ 152 | manifest-tool \ 153 | --username=oauth2accesstoken \ 154 | --password=$$(gcloud auth print-access-token) \ 155 | push from-args \ 156 | --platforms "$$platforms" \ 157 | --template $(REGISTRY)/$(BIN):$(VERSION)__OS_ARCH \ 158 | --target $(REGISTRY)/$(BIN):$(VERSION) 159 | 160 | version: 161 | @echo $(VERSION) 162 | 163 | test: $(BUILD_DIRS) 164 | @docker run \ 165 | -i \ 166 | --rm \ 167 | -u $$(id -u):$$(id -g) \ 168 | -v $$(pwd):/src \ 169 | -w /src \ 170 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ 171 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ 172 | -v $$(pwd)/.go/cache:/.cache \ 173 | -v $$(pwd)/config:/config \ 174 | -v $$(pwd)/cmd/blog_backend/test_data:/test_data \ 175 | --env HTTP_PROXY=$(HTTP_PROXY) \ 176 | --env HTTPS_PROXY=$(HTTPS_PROXY) \ 177 | $(TEST_IMAGE) \ 178 | /bin/sh -c " \ 179 | ARCH=$(ARCH) \ 180 | OS=$(OS) \ 181 | VERSION=$(VERSION) \ 182 | ./build/test.sh $(SRC_DIRS) \ 183 | " 184 | 185 | ci: $(BUILD_DIRS) 186 | @docker run \ 187 | -i \ 188 | --rm \ 189 | -u $$(id -u):$$(id -g) \ 190 | -v $$(pwd):/src \ 191 | -w /src \ 192 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ 193 | -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ 194 | -v $$(pwd)/.go/cache:/.cache \ 195 | -v $$(pwd)/reports:/reports \ 196 | -v $$(pwd)/config:/config \ 197 | -v $$(pwd)/cmd/blog_backend/test_data:/test_data \ 198 | -v $$(pwd)/:/coverage \ 199 | --env HTTP_PROXY=$(HTTP_PROXY) \ 200 | --env HTTPS_PROXY=$(HTTPS_PROXY) \ 201 | $(TEST_IMAGE) \ 202 | /bin/sh -c " \ 203 | ARCH=$(ARCH) \ 204 | OS=$(OS) \ 205 | VERSION=$(VERSION) \ 206 | ./build/test_ci.sh $(SRC_DIRS) \ 207 | " 208 | 209 | $(BUILD_DIRS): 210 | @mkdir -p $@ 211 | 212 | clean: container-clean bin-clean 213 | 214 | container-clean: 215 | rm -rf .container-* .dockerfile-* .push-* 216 | 217 | bin-clean: 218 | rm -rf .go bin 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Go Backend for Personal Website & Blog 2 | 3 | General Quality and Build: 4 | 5 | [![Build Status](https://travis-ci.com/MartinHeinz/blog-backend.svg?branch=master)](https://travis-ci.com/MartinHeinz/blog-backend) 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MartinHeinz_blog-backend&metric=alert_status)](https://sonarcloud.io/dashboard?id=MartinHeinz_blog-backend) 7 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=MartinHeinz_blog-backend&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=MartinHeinz_blog-backend) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/6bfaf0c31bdf6fd1fc7a/maintainability)](https://codeclimate.com/github/MartinHeinz/blog-backend/maintainability) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/728b4690245b4f768bd73773c06b735e)](https://app.codacy.com/app/MartinHeinz/blog-backend?utm_source=github.com&utm_medium=referral&utm_content=MartinHeinz/blog-backend&utm_campaign=Badge_Grade_Dashboard) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/MartinHeinz/blog-backend)](https://goreportcard.com/report/github.com/MartinHeinz/blog-backend) 11 | [![Test Coverage](https://api.codeclimate.com/v1/badges/6bfaf0c31bdf6fd1fc7a/test_coverage)](https://codeclimate.com/github/MartinHeinz/blog-backend/test_coverage) 12 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ -z "${OS:-}" ]; then 8 | echo "OS must be set" 9 | exit 1 10 | fi 11 | if [ -z "${ARCH:-}" ]; then 12 | echo "ARCH must be set" 13 | exit 1 14 | fi 15 | if [ -z "${VERSION:-}" ]; then 16 | echo "VERSION must be set" 17 | exit 1 18 | fi 19 | 20 | export CGO_ENABLED=0 21 | export GOARCH="${ARCH}" 22 | export GOOS="${OS}" 23 | export GO111MODULE=on 24 | export GOFLAGS="-mod=vendor" 25 | 26 | go install \ 27 | -installsuffix "static" \ 28 | -ldflags "-X $(go list -m)/pkg/version.VERSION=${VERSION}" \ 29 | ./... 30 | -------------------------------------------------------------------------------- /build/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export CGO_ENABLED=1 8 | export GO111MODULE=on 9 | export GOFLAGS="-mod=vendor" 10 | 11 | TARGETS=$(for d in "$@"; do echo ./$d/...; done) 12 | 13 | echo "Running tests:" 14 | go test -installsuffix "static" ${TARGETS} 2>&1 15 | echo 16 | 17 | echo -n "Checking gofmt: " 18 | ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) 19 | if [ -n "${ERRS}" ]; then 20 | echo "FAIL - the following files need to be gofmt'ed:" 21 | for e in ${ERRS}; do 22 | echo " $e" 23 | done 24 | echo 25 | exit 1 26 | fi 27 | echo "PASS" 28 | echo 29 | 30 | echo -n "Checking go vet: " 31 | ERRS=$(go vet ${TARGETS} 2>&1 || true) 32 | if [ -n "${ERRS}" ]; then 33 | echo "FAIL" 34 | echo "${ERRS}" 35 | echo 36 | exit 1 37 | fi 38 | echo "PASS" 39 | echo 40 | -------------------------------------------------------------------------------- /build/test_ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export CGO_ENABLED=1 8 | export GO111MODULE=on 9 | export GOFLAGS="-mod=vendor" 10 | 11 | TARGETS=$(for d in "$@"; do echo ./$d/...; done) 12 | 13 | echo "Running tests and Generating reports..." 14 | go test -coverprofile=/reports/coverage.out -installsuffix "static" ${TARGETS} -json > /reports/test-report.out 15 | cp /reports/coverage.out /coverage/c.out 16 | echo 17 | echo "Coverage:" 18 | go tool cover -func=/coverage/c.out 19 | echo 20 | 21 | echo -n "Checking gofmt: " 22 | ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) 23 | if [ -n "${ERRS}" ]; then 24 | echo "FAIL - the following files need to be gofmt'ed:" 25 | for e in ${ERRS}; do 26 | echo " $e" 27 | done 28 | echo 29 | exit 1 30 | fi 31 | echo "PASS" 32 | echo 33 | 34 | echo -n "Checking go vet: " 35 | ERRS=$(go vet ${TARGETS} 2>&1 | tee "/reports/vet.out" || true) 36 | if [ -n "${ERRS}" ]; then 37 | echo "FAIL" 38 | echo "${ERRS}" 39 | echo 40 | exit 1 41 | fi 42 | echo "PASS" 43 | echo 44 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/apis_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "bytes" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 6 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 7 | "github.com/gin-gonic/gin" 8 | "github.com/stretchr/testify/assert" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | type apiTestCase struct { 16 | tag string 17 | method string 18 | urlToServe string 19 | urlToHit string 20 | body string 21 | function gin.HandlerFunc 22 | status int 23 | responseFilePath string 24 | } 25 | 26 | func newRouter() *gin.Engine { 27 | gin.SetMode(gin.TestMode) 28 | router := gin.New() 29 | config.Config.DB = test_data.ResetDB() 30 | 31 | return router 32 | } 33 | 34 | func testAPI(router *gin.Engine, method string, urlToServe string, urlToHit string, function gin.HandlerFunc, body string) *httptest.ResponseRecorder { 35 | router.Handle(method, urlToServe, function) 36 | res := httptest.NewRecorder() 37 | req, _ := http.NewRequest(method, urlToHit, bytes.NewBufferString(body)) 38 | router.ServeHTTP(res, req) 39 | return res 40 | } 41 | 42 | func runAPITests(t *testing.T, tests []apiTestCase) { 43 | for _, test := range tests { 44 | router := newRouter() 45 | res := testAPI(router, test.method, test.urlToServe, test.urlToHit, test.function, test.body) 46 | assert.Equal(t, test.status, res.Code, test.tag) 47 | if test.responseFilePath != "" { 48 | response, _ := ioutil.ReadFile(test.responseFilePath) 49 | assert.JSONEq(t, string(response), res.Body.String(), test.tag) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/book.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/daos" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/services" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | // GetBooks is function for endpoint /api/v1/books to get all books 12 | func GetBooks(c *gin.Context) { 13 | s := services.NewBookService(daos.NewBookDAO()) 14 | if books, err := s.FindAll(); err != nil { 15 | c.AbortWithStatus(http.StatusNotFound) 16 | log.Println(err) 17 | } else { 18 | c.JSON(http.StatusOK, gin.H{ 19 | "books": books, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/book_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestBook(t *testing.T) { 10 | path := test_data.GetTestCaseFolder() 11 | runAPITests(t, []apiTestCase{ 12 | {"t1 - get all books", "GET", "/books/", "/books/", "", GetBooks, http.StatusOK, path + "/book_t1.json"}, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/email.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 6 | "github.com/gin-gonic/gin" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | // Add subscriber to mailing list at /api/v1/newsletter/subscribe/ 13 | func AddSubscriber(c *gin.Context) { 14 | url := "https://api.mailerlite.com/api/v2/groups/106705303/subscribers" 15 | 16 | req, err := http.NewRequest("POST", url, c.Request.Body) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | req.Header.Set("Content-Type", "application/json") 21 | req.Header.Set("X-MailerLite-ApiKey", config.Config.APIKey) 22 | client := &http.Client{} 23 | resp, err := client.Do(req) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer resp.Body.Close() 28 | 29 | if resp.StatusCode == http.StatusUnauthorized { 30 | log.Println("Status 401: Check API key in environment variables.") 31 | c.JSON(http.StatusUnauthorized, gin.H{}) 32 | } else { 33 | body, _ := ioutil.ReadAll(resp.Body) 34 | log.Println(fmt.Sprintf("New subscriber added: %s", string(body))) 35 | c.JSON(resp.StatusCode, gin.H{}) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/post.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/daos" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/services" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | // GetPost is function for endpoint /api/v1/posts to get Post by ID 13 | func GetPost(c *gin.Context) { 14 | s := services.NewPostService(daos.NewPostDAO()) 15 | id, _ := strconv.ParseUint(c.Param("id"), 10, 32) 16 | if post, err := s.Get(uint(id)); err != nil { 17 | c.AbortWithStatus(http.StatusNotFound) 18 | log.Println(err) 19 | } else { 20 | c.JSON(http.StatusOK, post) 21 | } 22 | } 23 | 24 | // GetPosts is function for endpoint /api/v1/posts to get all posts 25 | func GetPosts(c *gin.Context) { 26 | s := services.NewPostService(daos.NewPostDAO()) 27 | if posts, err := s.FindAll(); err != nil { 28 | c.AbortWithStatus(http.StatusNotFound) 29 | log.Println(err) 30 | } else { 31 | c.JSON(http.StatusOK, gin.H{ 32 | "posts": posts, 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/post_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestPost(t *testing.T) { 10 | path := test_data.GetTestCaseFolder() 11 | runAPITests(t, []apiTestCase{ 12 | {"t1 - get a Post", "GET", "/posts/:id", "/posts/1", "", GetPost, http.StatusOK, path + "/post_t1.json"}, 13 | {"t2 - get a Post not Present", "GET", "/posts/:id", "/posts/9999", "", GetPost, http.StatusNotFound, ""}, 14 | {"t3 - get all posts", "GET", "/posts/", "/posts/", "", GetPosts, http.StatusOK, path + "/post_t3.json"}, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/project.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/daos" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/services" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | // GetProjects is function for endpoint /api/v1/projects to get all project 12 | func GetProjects(c *gin.Context) { 13 | s := services.NewProjectService(daos.NewProjectDAO()) 14 | if projects, err := s.FindAll(); err != nil { 15 | c.AbortWithStatus(http.StatusNotFound) 16 | log.Println(err) 17 | } else { 18 | c.JSON(http.StatusOK, gin.H{ 19 | "projects": projects, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/project_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestProject(t *testing.T) { 10 | path := test_data.GetTestCaseFolder() 11 | runAPITests(t, []apiTestCase{ 12 | {"t1 - get all projects", "GET", "/projects/", "/projects/", "", GetProjects, http.StatusOK, path + "/project_t1.json"}, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/section.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/daos" 6 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/services" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | // GetSections is function for endpoint /api/v1/tags to get all sections by post_id 13 | func GetSections(c *gin.Context) { 14 | s := services.NewSectionService(daos.NewSectionDAO()) 15 | id, _ := strconv.ParseUint(c.Param("post_id"), 10, 32) 16 | if sections, err := s.FindAll(uint(id)); err != nil { 17 | c.AbortWithStatus(http.StatusNotFound) 18 | fmt.Println(err) 19 | } else { 20 | c.JSON(http.StatusOK, gin.H{ 21 | "sections": sections, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/section_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestSection(t *testing.T) { 10 | path := test_data.GetTestCaseFolder() 11 | runAPITests(t, []apiTestCase{ 12 | {"t1 - get all sections for post", "GET", "/sections/:post_id", "/sections/1", "", GetSections, http.StatusOK, path + "/section_t1.json"}, 13 | {"t2 - try to get sections, that are not present", "GET", "/sections/:post_id", "/sections/9999", "", GetSections, http.StatusNotFound, ""}, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/tag.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/daos" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/services" 6 | "github.com/gin-gonic/gin" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | // GetTags is function for endpoint /api/v1/tags to get all tags by post_id 13 | func GetTags(c *gin.Context) { 14 | s := services.NewTagService(daos.NewTagDAO()) 15 | id, _ := strconv.ParseUint(c.Param("post_id"), 10, 32) 16 | if tags, err := s.FindAll(uint(id)); err != nil { 17 | c.AbortWithStatus(http.StatusNotFound) 18 | log.Println(err) 19 | } else { 20 | c.JSON(http.StatusOK, gin.H{ 21 | "tags": tags, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/blog_backend/apis/tag_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestTag(t *testing.T) { 10 | path := test_data.GetTestCaseFolder() 11 | runAPITests(t, []apiTestCase{ 12 | {"t1 - get all tags for post", "GET", "/tags/:post_id", "/tags/1", "", GetTags, http.StatusOK, path + "/tag_t1.json"}, 13 | {"t2 - try to get tags, that are not present", "GET", "/tags/:post_id", "/tags/9999", "", GetTags, http.StatusNotFound, ""}, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/blog_backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var Config appConfig 11 | 12 | type appConfig struct { 13 | // the shared DB ORM object 14 | DB *gorm.DB 15 | // the error thrown be GORM when using DB ORM object 16 | DBErr error 17 | // the path to the error message file. Defaults to "config/errors.yaml" 18 | ErrorFile string `mapstructure:"error_file"` 19 | // the server port. Defaults to 8080 20 | ServerPort int `mapstructure:"server_port"` 21 | // the data source name (DSN) for connecting to the database. required. 22 | DSN string `mapstructure:"dsn"` 23 | // the signing method for JWT. Defaults to "HS256" 24 | JWTSigningMethod string `mapstructure:"jwt_signing_method"` 25 | // JWT signing key. required. 26 | JWTSigningKey string `mapstructure:"jwt_signing_key"` 27 | // JWT verification key. required. 28 | JWTVerificationKey string `mapstructure:"jwt_verification_key"` 29 | // Certificate file for HTTPS 30 | CertFile string `mapstructure:"cert_file"` 31 | // Private key file for HTTPS 32 | KeyFile string `mapstructure:"key_file"` 33 | // MailerLite API Key 34 | APIKey string `mapstructure:"api_key"` 35 | } 36 | 37 | func LoadConfig(configPaths ...string) error { 38 | v := viper.New() 39 | v.SetConfigName("server") 40 | v.SetConfigType("yaml") 41 | v.SetEnvPrefix("backend") 42 | v.AutomaticEnv() 43 | v.SetDefault("error_file", "/config/errors.yaml") 44 | v.SetDefault("server_port", 8080) 45 | v.SetDefault("jwt_signing_method", "HS256") 46 | v.SetDefault("cert_file", "/etc/certs/fullchain.pem") 47 | v.SetDefault("key_file", "/etc/certs/privkey.pem") 48 | for _, path := range configPaths { 49 | v.AddConfigPath(path) 50 | } 51 | if err := v.ReadInConfig(); err != nil { 52 | return fmt.Errorf("failed to read the configuration file: %s", err) 53 | } 54 | if err := v.Unmarshal(&Config); err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/book.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | // BookDAO persists book data in database 9 | type BookDAO struct{} 10 | 11 | // NewBookDAO creates a new BookDAO 12 | func NewBookDAO() *BookDAO { 13 | return &BookDAO{} 14 | } 15 | 16 | func (dao *BookDAO) FindAll() []models.Book { 17 | var books []models.Book 18 | config.Config.DB.Find(&books) 19 | return books 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/book_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestBookDAO_FindAll(t *testing.T) { 11 | config.Config.DB = test_data.ResetDB() 12 | dao := NewBookDAO() 13 | 14 | books := dao.FindAll() 15 | 16 | assert.Equal(t, 4, len(books)) 17 | } 18 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/post.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | // PostDAO persists post data in database 9 | type PostDAO struct{} 10 | 11 | // NewPostDAO creates a new PostDAO 12 | func NewPostDAO() *PostDAO { 13 | return &PostDAO{} 14 | } 15 | 16 | func (dao *PostDAO) Get(id uint) (*models.Post, error) { 17 | var post models.Post 18 | err := config.Config.DB.Where("id = ?", id). 19 | Preload("Tags"). 20 | Preload("Sections"). 21 | First(&post). 22 | Error 23 | 24 | return &post, err 25 | } 26 | 27 | func (dao *PostDAO) FindAll() []models.Post { 28 | var posts []models.Post 29 | config.Config.DB.Order("posted_on DESC"). 30 | Find(&posts) 31 | return posts 32 | } 33 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/post_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestPostDAO_Get(t *testing.T) { 12 | config.Config.DB = test_data.ResetDB() 13 | dao := NewPostDAO() 14 | 15 | post, err := dao.Get(1) 16 | 17 | expected := map[string]string{"Title": "First Blog Post", "Text": "This is blog about something."} 18 | 19 | assert.Nil(t, err) 20 | assert.Equal(t, expected["Title"], post.Title) 21 | assert.Equal(t, expected["Text"], post.Text) 22 | assert.Equal(t, 3, len(post.Tags)) 23 | assert.Equal(t, 2, len(post.Sections)) 24 | } 25 | 26 | func TestPostDAO_GetNotPresent(t *testing.T) { 27 | config.Config.DB = test_data.ResetDB() 28 | dao := NewPostDAO() 29 | 30 | post, err := dao.Get(9999) 31 | 32 | assert.NotNil(t, err) 33 | assert.Equal(t, "", post.Title) 34 | assert.Equal(t, "", post.Text) 35 | } 36 | 37 | func TestPostDAO_FindAll(t *testing.T) { 38 | config.Config.DB = test_data.ResetDB() 39 | dao := NewPostDAO() 40 | 41 | posts := dao.FindAll() 42 | 43 | assert.Equal(t, 3, len(posts)) 44 | } 45 | 46 | func TestPostDAO_FindAllOrdered(t *testing.T) { 47 | timeFormat := "2006-01-02 15:04:05" 48 | config.Config.DB = test_data.ResetDB() 49 | dao := NewPostDAO() 50 | 51 | posts := dao.FindAll() 52 | 53 | firstPostDate, _ := time.Parse(timeFormat, "2019-05-30 19:00:00") 54 | assert.Equal(t, firstPostDate, posts[0].PostedOn) 55 | secondPostDate, _ := time.Parse(timeFormat, "2019-02-24 13:00:00") 56 | assert.Equal(t, secondPostDate, posts[1].PostedOn) 57 | thirdPostDate, _ := time.Parse(timeFormat, "2018-08-24 14:00:00") 58 | assert.Equal(t, thirdPostDate, posts[2].PostedOn) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/project.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | // ProjectDAO persists project data in database 9 | type ProjectDAO struct{} 10 | 11 | // NewProjectDAO creates a new ProjectDAO 12 | func NewProjectDAO() *ProjectDAO { 13 | return &ProjectDAO{} 14 | } 15 | 16 | func (dao *ProjectDAO) FindAll() []models.Project { 17 | var projects []models.Project 18 | config.Config.DB.Preload("Tags").Find(&projects) 19 | return projects 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/project_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 6 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestProjectDAO_FindAll(t *testing.T) { 12 | config.Config.DB = test_data.ResetDB() 13 | dao := NewProjectDAO() 14 | 15 | projects := dao.FindAll() 16 | 17 | fmt.Printf("%v", projects) 18 | 19 | assert.Equal(t, 2, len(projects)) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/section.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | // TagDAO persists tag data in database 9 | type SectionDAO struct{} 10 | 11 | // NewSectionDAO creates a new SectionDAO 12 | func NewSectionDAO() *SectionDAO { 13 | return &SectionDAO{} 14 | } 15 | 16 | func (dao *SectionDAO) FindAll(postId uint) []models.Section { 17 | var sections []models.Section 18 | config.Config.DB.Where("post_id = ?", postId).Find(§ions) 19 | return sections 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/section_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSectionDAO_FindAll(t *testing.T) { 11 | config.Config.DB = test_data.ResetDB() 12 | dao := NewSectionDAO() 13 | 14 | sections := dao.FindAll(2) 15 | 16 | assert.Equal(t, 4, len(sections)) 17 | } 18 | 19 | func TestSectionDAO_FindEmpty(t *testing.T) { 20 | config.Config.DB = test_data.ResetDB() 21 | dao := NewSectionDAO() 22 | 23 | sections := dao.FindAll(9999) 24 | 25 | assert.Empty(t, sections) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/tag.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | // TagDAO persists tag data in database 9 | type TagDAO struct{} 10 | 11 | // NewTagDAO creates a new TagDAO 12 | func NewTagDAO() *TagDAO { 13 | return &TagDAO{} 14 | } 15 | 16 | func (dao *TagDAO) FindAll(postId uint) []models.Tag { 17 | var tags []models.Tag 18 | config.Config.DB.Where("post_id = ?", postId).Find(&tags) 19 | return tags 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/daos/tag_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/test_data" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestTagDAO_FindAll(t *testing.T) { 11 | config.Config.DB = test_data.ResetDB() 12 | dao := NewTagDAO() 13 | 14 | tags := dao.FindAll(1) 15 | 16 | assert.Equal(t, 3, len(tags)) 17 | } 18 | 19 | func TestTagDAO_FindEmpty(t *testing.T) { 20 | config.Config.DB = test_data.ResetDB() 21 | dao := NewTagDAO() 22 | 23 | tags := dao.FindAll(9999) 24 | 25 | assert.Empty(t, tags) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/blog_backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/apis" 6 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 7 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/middleware" 8 | brotli "github.com/anargu/gin-brotli" 9 | "github.com/gin-gonic/gin" 10 | "github.com/jinzhu/gorm" 11 | _ "github.com/jinzhu/gorm/dialects/postgres" 12 | ) 13 | 14 | func main() { 15 | // load application configurations 16 | if err := config.LoadConfig("./config"); err != nil { 17 | panic(fmt.Errorf("invalid application configuration: %s", err)) 18 | } 19 | 20 | // Creates a router without any middleware by default 21 | r := gin.New() 22 | 23 | // Global middleware 24 | // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. 25 | // By default gin.DefaultWriter = os.Stdout 26 | r.Use(gin.Logger()) 27 | 28 | // Recovery middleware recovers from any panics and writes a 500 if there was one. 29 | r.Use(gin.Recovery()) 30 | 31 | r.Use(middleware.CORSMiddleware()) 32 | r.Use(brotli.Brotli(brotli.DefaultCompression)) 33 | 34 | v1 := r.Group("/api/v1") 35 | { 36 | v1.GET("/posts/:id", apis.GetPost) 37 | v1.GET("/posts/", apis.GetPosts) 38 | v1.GET("/tags/:post_id", apis.GetTags) 39 | v1.GET("/sections/:post_id", apis.GetSections) 40 | v1.GET("/books/", apis.GetBooks) 41 | v1.GET("/projects/", apis.GetProjects) 42 | v1.POST("/newsletter/subscribe/", apis.AddSubscriber) 43 | } 44 | 45 | config.Config.DB, config.Config.DBErr = gorm.Open("postgres", config.Config.DSN) 46 | if config.Config.DBErr != nil { 47 | panic(config.Config.DBErr) 48 | } 49 | 50 | // config.Config.DB.AutoMigrate(&models.Post{}, &models.Project{}, &models.Section{}, &models.Tag{}, &models.Book{}) // This is needed for generation of schema for postgres image. 51 | 52 | defer config.Config.DB.Close() 53 | 54 | fmt.Println(fmt.Sprintf("Successfully connected to :%v", config.Config.DSN)) 55 | 56 | r.RunTLS(fmt.Sprintf(":%v", config.Config.ServerPort), config.Config.CertFile, config.Config.KeyFile) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/blog_backend/middleware/brotli_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | brotli "github.com/anargu/gin-brotli" 6 | "github.com/gin-gonic/gin" 7 | "github.com/stretchr/testify/assert" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestBrotliMiddleware(t *testing.T) { 16 | resp := httptest.NewRecorder() 17 | gin.SetMode(gin.TestMode) 18 | c, r := gin.CreateTestContext(resp) 19 | 20 | r.Use(brotli.Brotli(brotli.DefaultCompression)) 21 | 22 | r.GET("/test", func(c *gin.Context) { 23 | c.String(http.StatusOK, fmt.Sprintf("World at %s", time.Now())) 24 | }) 25 | c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil) 26 | c.Request.Header.Set("Content-Type", "application/json") 27 | c.Request.Header.Set("Accept-Encoding", "br") 28 | r.ServeHTTP(resp, c.Request) 29 | 30 | log.Println(resp.Header()) 31 | assert.Equal(t, "br", resp.Header().Get("Content-Encoding")) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/blog_backend/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func CORSMiddleware() gin.HandlerFunc { 6 | return func(c *gin.Context) { 7 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 8 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 9 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") 10 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") 11 | 12 | if c.Request.Method == "OPTIONS" { 13 | c.AbortWithStatus(204) 14 | return 15 | } 16 | 17 | c.Next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/blog_backend/middleware/cors_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestCORSMiddlewareGET(t *testing.T) { 12 | resp := httptest.NewRecorder() 13 | gin.SetMode(gin.TestMode) 14 | c, r := gin.CreateTestContext(resp) 15 | 16 | r.Use(CORSMiddleware()) 17 | 18 | r.GET("/test", func(c *gin.Context) { 19 | c.Status(200) 20 | }) 21 | c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil) 22 | r.ServeHTTP(resp, c.Request) 23 | 24 | assert.Equal(t, "*", resp.Header().Get("Access-Control-Allow-Origin")) 25 | assert.Equal(t, "true", resp.Header().Get("Access-Control-Allow-Credentials")) 26 | assert.Equal(t, "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With", 27 | resp.Header().Get("Access-Control-Allow-Headers")) 28 | assert.Equal(t, "POST, OPTIONS, GET, PUT", resp.Header().Get("Access-Control-Allow-Methods")) 29 | } 30 | 31 | func TestCORSMiddlewareOPTIONS(t *testing.T) { 32 | resp := httptest.NewRecorder() 33 | gin.SetMode(gin.TestMode) 34 | c, r := gin.CreateTestContext(resp) 35 | 36 | r.Use(CORSMiddleware()) 37 | 38 | r.OPTIONS("/test", func(c *gin.Context) {}) 39 | 40 | c.Request, _ = http.NewRequest(http.MethodOptions, "/test", nil) 41 | r.ServeHTTP(resp, c.Request) 42 | 43 | assert.Equal(t, http.StatusNoContent, resp.Code) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/blog_backend/models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Model definition same as gorm.Model, but including column and json tags 8 | type Model struct { 9 | ID uint `gorm:"primary_key;column:id" json:"id"` 10 | CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` 11 | UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` 12 | DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"` 13 | } 14 | 15 | // Blog Post 16 | type Post struct { 17 | Model 18 | Title string `gorm:"column:title" json:"title"` 19 | Text string `gorm:"column:text" json:"text"` 20 | Author string `gorm:"column:author" json:"author"` 21 | Next *Post `gorm:"column:next" json:"next"` 22 | NextPostID uint `gorm:"type:int REFERENCES posts(id) ON DELETE CASCADE;column:next_post_id" json:"next_post_id"` 23 | Previous *Post `gorm:"column:previous" json:"previous"` 24 | PreviousPostID uint `gorm:"type:int REFERENCES posts(id) ON DELETE CASCADE;column:previous_post_id" json:"previous_post_id"` 25 | PostedOn time.Time `gorm:"column:posted_on" json:"posted_on"` 26 | Sections []Section `gorm:"column:sections" json:"sections"` 27 | Tags []Tag `gorm:"column:tags" json:"tags"` 28 | } 29 | 30 | // Section of Blog Post (headings) 31 | type Section struct { 32 | Model 33 | PostID uint `gorm:"type:int REFERENCES posts(id) ON DELETE CASCADE;column:post_id" json:"post_id"` 34 | Name string `gorm:"column:name" json:"name"` 35 | } 36 | 37 | // Project that I developed 38 | type Project struct { 39 | Model 40 | Name string `gorm:"column:name" json:"name"` 41 | ThumbnailPictureURL string `gorm:"column:thumbnail_url" json:"src"` 42 | URL string `gorm:"column:url" json:"url"` 43 | Description string `gorm:"column:description" json:"description"` 44 | Tags []Tag `gorm:"column:tags" json:"tags"` 45 | } 46 | 47 | // Tag of Blog Post (hashtag) 48 | type Tag struct { 49 | Model 50 | PostID uint `gorm:"type:int REFERENCES posts(id) ON DELETE CASCADE;column:post_id" json:"post_id"` 51 | ProjectID uint `gorm:"type:int REFERENCES projects(id) ON DELETE CASCADE;column:project_id" json:"project_id"` 52 | Name string `gorm:"column:name" json:"name"` 53 | } 54 | 55 | // Book that I Read 56 | type Book struct { 57 | Model 58 | Title string `gorm:"column:title" json:"title"` 59 | CoverPictureURL string `gorm:"column:cover_url" json:"src"` 60 | URL string `gorm:"column:url" json:"url"` 61 | AlternativeText string `gorm:"column:alt" json:"alt"` 62 | } 63 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/book.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | type bookDAO interface { 9 | FindAll() []models.Book 10 | } 11 | 12 | type BookService struct { 13 | dao bookDAO 14 | } 15 | 16 | // NewBookService creates a new BookService with the given book DAO. 17 | func NewBookService(dao bookDAO) *BookService { 18 | return &BookService{dao} 19 | } 20 | 21 | func (s *BookService) FindAll() ([]models.Book, error) { 22 | books := s.dao.FindAll() 23 | err := fmt.Errorf("no books found") 24 | if len(books) > 0 { 25 | return books, nil 26 | } 27 | return books, err 28 | } 29 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/book_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewBookService(t *testing.T) { 10 | dao := newMockBookDAO() 11 | s := NewBookService(dao) 12 | assert.Equal(t, dao, s.dao) 13 | } 14 | 15 | func TestBookService_FindAll(t *testing.T) { 16 | s := NewBookService(newMockBookDAO()) 17 | books, err := s.FindAll() 18 | if assert.Nil(t, err) && assert.NotEmpty(t, books) { 19 | assert.Equal(t, 3, len(books)) 20 | } 21 | 22 | s = NewBookService(newMockBookDAOEmpty()) 23 | books, err = s.FindAll() 24 | assert.NotNil(t, err) 25 | assert.Empty(t, books) 26 | } 27 | 28 | func newMockBookDAO() bookDAO { 29 | return &mockBookDAO{ 30 | records: []models.Book{ 31 | {Model: models.Model{ID: 1}, Title: "The Go Programming Language", CoverPictureURL: "https://www.gopl.io/cover.png", URL: "https://www.gopl.io/", AlternativeText: "The Go Programming Language"}, 32 | {Model: models.Model{ID: 2}, Title: "Clean Code", CoverPictureURL: "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1436202607i/3735293._SX318_.jpg", URL: "https://www.goodreads.com/book/show/3735293-clean-code", AlternativeText: "Clean Code: A Handbook of Agile Software Craftsmanship"}, 33 | {Model: models.Model{ID: 3}, Title: "Software Craftsmanship", CoverPictureURL: "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1370897661i/18054154.jpg", URL: "https://www.goodreads.com/book/show/18054154-software-craftsmanship", AlternativeText: "The Software Craftsman: Professionalism, Pragmatism, Pride"}, 34 | }, 35 | } 36 | } 37 | 38 | func newMockBookDAOEmpty() bookDAO { 39 | return &mockBookDAO{ 40 | records: []models.Book{}, 41 | } 42 | } 43 | 44 | func (m *mockBookDAO) FindAll() []models.Book { 45 | books := m.records 46 | return books 47 | } 48 | 49 | type mockBookDAO struct { 50 | records []models.Book 51 | } 52 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/post.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | type postDAO interface { 9 | Get(id uint) (*models.Post, error) 10 | FindAll() []models.Post 11 | } 12 | 13 | type PostService struct { 14 | dao postDAO 15 | } 16 | 17 | // NewPostService creates a new PostService with the given post DAO. 18 | func NewPostService(dao postDAO) *PostService { 19 | return &PostService{dao} 20 | } 21 | 22 | func (s *PostService) Get(id uint) (*models.Post, error) { 23 | return s.dao.Get(id) 24 | } 25 | 26 | func (s *PostService) FindAll() ([]models.Post, error) { 27 | posts := s.dao.FindAll() 28 | err := fmt.Errorf("no posts found") 29 | if len(posts) > 0 { 30 | return posts, nil 31 | } 32 | return posts, err 33 | } 34 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/post_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestNewPostService(t *testing.T) { 11 | dao := newMockPostDAO() 12 | s := NewPostService(dao) 13 | assert.Equal(t, dao, s.dao) 14 | } 15 | 16 | func TestPostService_Get(t *testing.T) { 17 | s := NewPostService(newMockPostDAO()) 18 | post, err := s.Get(1) 19 | if assert.Nil(t, err) && assert.NotNil(t, post) { 20 | assert.Equal(t, "Test Title", post.Title) 21 | } 22 | 23 | post, err = s.Get(100) 24 | assert.NotNil(t, err) 25 | } 26 | 27 | func TestPostService_FindAll(t *testing.T) { 28 | s := NewPostService(newMockPostDAO()) 29 | books, err := s.FindAll() 30 | if assert.Nil(t, err) && assert.NotEmpty(t, books) { 31 | assert.Equal(t, 2, len(books)) 32 | } 33 | 34 | s = NewPostService(newMockPostDAOEmpty()) 35 | books, err = s.FindAll() 36 | assert.NotNil(t, err) 37 | assert.Empty(t, books) 38 | } 39 | 40 | func newMockPostDAO() postDAO { 41 | return &mockPostDAO{ 42 | records: []models.Post{ 43 | {Model: models.Model{ID: 1}, Title: "Test Title", Text: "Test Text."}, 44 | {Model: models.Model{ID: 2}, Title: "Test Title 2", Text: "Test Text 2."}, 45 | }, 46 | } 47 | } 48 | 49 | func newMockPostDAOEmpty() postDAO { 50 | return &mockPostDAO{ 51 | records: []models.Post{}, 52 | } 53 | } 54 | 55 | func (m *mockPostDAO) Get(id uint) (*models.Post, error) { 56 | for _, record := range m.records { 57 | if record.ID == id { 58 | return &record, nil 59 | } 60 | } 61 | return nil, errors.New("not found") 62 | } 63 | 64 | func (m *mockPostDAO) FindAll() []models.Post { 65 | posts := m.records 66 | return posts 67 | } 68 | 69 | type mockPostDAO struct { 70 | records []models.Post 71 | } 72 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/project.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | type projectDAO interface { 9 | FindAll() []models.Project 10 | } 11 | 12 | type ProjectService struct { 13 | dao projectDAO 14 | } 15 | 16 | // NewProjectService creates a new ProjectService with the given project DAO. 17 | func NewProjectService(dao projectDAO) *ProjectService { 18 | return &ProjectService{dao} 19 | } 20 | 21 | func (s *ProjectService) FindAll() ([]models.Project, error) { 22 | projects := s.dao.FindAll() 23 | err := fmt.Errorf("no projects found") 24 | if len(projects) > 0 { 25 | return projects, nil 26 | } 27 | return projects, err 28 | } 29 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/project_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewProjectService(t *testing.T) { 10 | dao := newMockProjectDAO() 11 | s := NewProjectService(dao) 12 | assert.Equal(t, dao, s.dao) 13 | } 14 | 15 | func TestProjectService_FindAll(t *testing.T) { 16 | s := NewProjectService(newMockProjectDAO()) 17 | projects, err := s.FindAll() 18 | if assert.Nil(t, err) && assert.NotEmpty(t, projects) { 19 | assert.Equal(t, 2, len(projects)) 20 | } 21 | 22 | s = NewProjectService(newMockProjectDAOEmpty()) 23 | projects, err = s.FindAll() 24 | assert.NotNil(t, err) 25 | assert.Empty(t, projects) 26 | } 27 | 28 | func newMockProjectDAO() projectDAO { 29 | return &mockProjectDAO{ 30 | records: []models.Project{ 31 | {Model: models.Model{ID: 1}, Name: "IoT Cloud", ThumbnailPictureURL: "https://via.placeholder.com/150", URL: "https://github.com/MartinHeinz/IoT-Cloud", Description: "Cloud framework for IoT (Internet of Things), which focuses on security and privacy of its users, their devices and data"}, 32 | {Model: models.Model{ID: 2}, Name: "Blog & Personal Website", ThumbnailPictureURL: "https://via.placeholder.com/150", URL: "https://github.com/MartinHeinz/blog-backend", Description: "This website. Goal of this project was to learn Go and Vue.js and as a byproduct I created personal website and blog."}, 33 | }, 34 | } 35 | } 36 | 37 | func newMockProjectDAOEmpty() projectDAO { 38 | return &mockProjectDAO{ 39 | records: []models.Project{}, 40 | } 41 | } 42 | 43 | func (m *mockProjectDAO) FindAll() []models.Project { 44 | projects := m.records 45 | return projects 46 | } 47 | 48 | type mockProjectDAO struct { 49 | records []models.Project 50 | } 51 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/section.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | type sectionDAO interface { 9 | FindAll(id uint) []models.Section 10 | } 11 | 12 | type SectionService struct { 13 | dao sectionDAO 14 | } 15 | 16 | // NewSectionService creates a new SectionService with the given section DAO. 17 | func NewSectionService(dao sectionDAO) *SectionService { 18 | return &SectionService{dao} 19 | } 20 | 21 | func (s *SectionService) FindAll(postId uint) ([]models.Section, error) { 22 | sections := s.dao.FindAll(postId) 23 | err := fmt.Errorf("no sections found for post_id: %v", postId) 24 | if len(sections) > 0 { 25 | return sections, nil 26 | } 27 | return sections, err 28 | } 29 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/section_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewSectionService(t *testing.T) { 10 | dao := newMockSectionDAO() 11 | s := NewSectionService(dao) 12 | assert.Equal(t, dao, s.dao) 13 | } 14 | 15 | func TestSectionService_FindAll(t *testing.T) { 16 | s := NewSectionService(newMockSectionDAO()) 17 | sections, err := s.FindAll(1) 18 | if assert.Nil(t, err) && assert.NotEmpty(t, sections) { 19 | assert.Equal(t, 3, len(sections)) 20 | } 21 | 22 | sections, err = s.FindAll(100) 23 | assert.NotNil(t, err) 24 | assert.Empty(t, sections) 25 | } 26 | 27 | func newMockSectionDAO() sectionDAO { 28 | return &mockSectionDAO{ 29 | records: []models.Section{ 30 | {Model: models.Model{ID: 1}, PostID: 1, Name: "Title"}, 31 | {Model: models.Model{ID: 2}, PostID: 2, Name: "Title"}, 32 | {Model: models.Model{ID: 3}, PostID: 3, Name: "Title"}, 33 | {Model: models.Model{ID: 4}, PostID: 1, Name: "Subsection"}, 34 | {Model: models.Model{ID: 5}, PostID: 1, Name: "Subsection 2"}, 35 | }, 36 | } 37 | } 38 | 39 | func (m *mockSectionDAO) FindAll(postId uint) []models.Section { 40 | var sections []models.Section 41 | 42 | for _, record := range m.records { 43 | if record.PostID == postId { 44 | sections = append(sections, record) 45 | } 46 | } 47 | return sections 48 | } 49 | 50 | type mockSectionDAO struct { 51 | records []models.Section 52 | } 53 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/tag.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 6 | ) 7 | 8 | type tagDAO interface { 9 | FindAll(id uint) []models.Tag 10 | } 11 | 12 | type TagService struct { 13 | dao tagDAO 14 | } 15 | 16 | // NewTagService creates a new TagService with the given tag DAO. 17 | func NewTagService(dao tagDAO) *TagService { 18 | return &TagService{dao} 19 | } 20 | 21 | func (s *TagService) FindAll(postId uint) ([]models.Tag, error) { 22 | tags := s.dao.FindAll(postId) 23 | err := fmt.Errorf("no tags found for post_id: %v", postId) 24 | if len(tags) > 0 { 25 | return tags, nil 26 | } 27 | return tags, err 28 | } 29 | -------------------------------------------------------------------------------- /cmd/blog_backend/services/tag_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewTagService(t *testing.T) { 10 | dao := newMockTagDAO() 11 | s := NewTagService(dao) 12 | assert.Equal(t, dao, s.dao) 13 | } 14 | 15 | func TestTagService_FindAll(t *testing.T) { 16 | s := NewTagService(newMockTagDAO()) 17 | tags, err := s.FindAll(1) 18 | if assert.Nil(t, err) && assert.NotEmpty(t, tags) { 19 | assert.Equal(t, 2, len(tags)) 20 | } 21 | 22 | tags, err = s.FindAll(100) 23 | assert.NotNil(t, err) 24 | assert.Empty(t, tags) 25 | } 26 | 27 | func newMockTagDAO() tagDAO { 28 | return &mockTagDAO{ 29 | records: []models.Tag{ 30 | {Model: models.Model{ID: 1}, PostID: 1, Name: "Python"}, 31 | {Model: models.Model{ID: 2}, PostID: 1, Name: "Golang"}, 32 | {Model: models.Model{ID: 3}, PostID: 3, Name: "Crypto"}, 33 | {Model: models.Model{ID: 4}, PostID: 2, Name: "Python"}, 34 | }, 35 | } 36 | } 37 | 38 | func (m *mockTagDAO) FindAll(postId uint) []models.Tag { 39 | var tags []models.Tag 40 | 41 | for _, record := range m.records { 42 | if record.PostID == postId { 43 | tags = append(tags, record) 44 | } 45 | } 46 | return tags 47 | } 48 | 49 | type mockTagDAO struct { 50 | records []models.Tag 51 | } 52 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/db.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('First Blog Post', 'This is blog about something.', 'Martin', '2018-08-24 14:00:00', null, null); 2 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('Second Blog Post', 'This is blog about something else...', 'Martin', '2019-02-24 13:00:00', null, null); 3 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('3rd Blog Post', 'Another dummy content', 'Martin', '2019-05-30 19:00:00', null, null); 4 | 5 | UPDATE posts SET next_post_id = 2, previous_post_id = null WHERE id = 1; 6 | UPDATE posts SET next_post_id = 3, previous_post_id = 1 WHERE id = 2; 7 | UPDATE posts SET next_post_id = null, previous_post_id = 2 WHERE id = 3; 8 | 9 | INSERT INTO sections (name, post_id) VALUES ('Title', 1); 10 | INSERT INTO sections (name, post_id) VALUES ('Subsection', 1); 11 | INSERT INTO sections (name, post_id) VALUES ('Intro', 2); 12 | INSERT INTO sections (name, post_id) VALUES ('Subsection', 2); 13 | INSERT INTO sections (name, post_id) VALUES ('Subsection 2', 2); 14 | INSERT INTO sections (name, post_id) VALUES ('Subsection 3', 2); 15 | INSERT INTO sections (name, post_id) VALUES ('Intro', 3); 16 | INSERT INTO sections (name, post_id) VALUES ('First Section', 3); 17 | 18 | INSERT INTO projects (name, thumbnail_url, url, description) VALUES ('IoT Cloud', 'https://via.placeholder.com/150', 'https://github.com/MartinHeinz/IoT-Cloud', 'Cloud framework for IoT (Internet of Things), which focuses on security and privacy of its users, their devices and data'); 19 | INSERT INTO projects (name, thumbnail_url, url, description) VALUES ('Blog & Personal Website', 'https://via.placeholder.com/150', 'https://github.com/MartinHeinz/blog-backend', 'This website. Goal of this project was to learn Go and Vue.js and as a byproduct I created personal website and blog.'); 20 | 21 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', 1, null); 22 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', 2, null); 23 | INSERT INTO tags (name, post_id, project_id) VALUES ('Crypto', 1, null); 24 | INSERT INTO tags (name, post_id, project_id) VALUES ('Golang', 1, null); 25 | INSERT INTO tags (name, post_id, project_id) VALUES ('Vue', 3, null); 26 | 27 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', null, 1); 28 | INSERT INTO tags (name, post_id, project_id) VALUES ('Cryptography', null, 1); 29 | INSERT INTO tags (name, post_id, project_id) VALUES ('Privacy', null, 1); 30 | INSERT INTO tags (name, post_id, project_id) VALUES ('IoT', null, 1); 31 | INSERT INTO tags (name, post_id, project_id) VALUES ('Vue', null, 2); 32 | INSERT INTO tags (name, post_id, project_id) VALUES ('Golang', null, 2); 33 | INSERT INTO tags (name, post_id, project_id) VALUES ('Docker', null, 2); 34 | 35 | INSERT INTO books (title, cover_url, url, alt) VALUES ('The Go Programming Language', 'https://www.gopl.io/cover.png', 'https://www.gopl.io/', 'The Go Programming Language'); 36 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Clean Code', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1436202607i/3735293._SX318_.jpg', 'https://www.goodreads.com/book/show/3735293-clean-code', 'Clean Code: A Handbook of Agile Software Craftsmanship'); 37 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Software Craftsmanship', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1370897661i/18054154.jpg', 'https://www.goodreads.com/book/show/18054154-software-craftsmanship', 'The Software Craftsman: Professionalism, Pragmatism, Pride'); 38 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Extreme Programming Explained', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1386925310i/67833.jpg', 'https://www.goodreads.com/book/show/67833.Extreme_Programming_Explained', 'Extreme Programming Explained: Embrace Change (The XP Series)'); 39 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/init.go: -------------------------------------------------------------------------------- 1 | package test_data 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/config" 6 | "github.com/MartinHeinz/blog-backend/cmd/blog_backend/models" 7 | "github.com/jinzhu/gorm" 8 | _ "github.com/jinzhu/gorm/dialects/sqlite" 9 | "io/ioutil" 10 | "strings" 11 | ) 12 | 13 | func init() { 14 | // the test may be started from the home directory or a subdirectory 15 | err := config.LoadConfig("/config") // on host use absolute path 16 | if err != nil { 17 | panic(err) 18 | } 19 | config.Config.DB, config.Config.DBErr = gorm.Open("sqlite3", ":memory:") 20 | config.Config.DB.Exec("PRAGMA foreign_keys = ON") // SQLite defaults to `foreign_keys = off'` 21 | if config.Config.DBErr != nil { 22 | panic(config.Config.DBErr) 23 | } 24 | 25 | config.Config.DB.AutoMigrate(&models.Post{}, &models.Project{}, &models.Section{}, &models.Tag{}, &models.Book{}) 26 | } 27 | 28 | func ResetDB() *gorm.DB { 29 | config.Config.DB.DropTableIfExists(&models.Post{}, &models.Section{}, &models.Tag{}, &models.Project{}, &models.Book{}) // Note: Order matters 30 | config.Config.DB.AutoMigrate(&models.Post{}, &models.Project{}, &models.Section{}, &models.Tag{}, &models.Book{}) 31 | if err := runSQLFile(config.Config.DB, getSQLFile()); err != nil { 32 | panic(fmt.Errorf("error while initializing test database: %s", err)) 33 | } 34 | return config.Config.DB 35 | } 36 | 37 | func getSQLFile() string { 38 | return "/test_data/db.sql" // on host use absolute path 39 | } 40 | 41 | func GetTestCaseFolder() string { 42 | return "/test_data/test_case_data" // on host use absolute path 43 | } 44 | 45 | func runSQLFile(db *gorm.DB, file string) error { 46 | s, err := ioutil.ReadFile(file) 47 | if err != nil { 48 | return err 49 | } 50 | lines := strings.Split(string(s), ";") 51 | for _, line := range lines { 52 | line = strings.TrimSpace(line) 53 | if line == "" { 54 | continue 55 | } 56 | if result := db.Exec(line); result.Error != nil { 57 | fmt.Println(line) 58 | return result.Error 59 | } 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/book_t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": [ 3 | { 4 | "id": 1, 5 | "created_at": "0001-01-01T00:00:00Z", 6 | "updated_at": "0001-01-01T00:00:00Z", 7 | "deleted_at": null, 8 | "title": "The Go Programming Language", 9 | "src": "https://www.gopl.io/cover.png", 10 | "url": "https://www.gopl.io/", 11 | "alt": "The Go Programming Language" 12 | }, 13 | { 14 | "id": 2, 15 | "created_at": "0001-01-01T00:00:00Z", 16 | "updated_at": "0001-01-01T00:00:00Z", 17 | "deleted_at": null, 18 | "title": "Clean Code", 19 | "src": "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1436202607i/3735293._SX318_.jpg", 20 | "url": "https://www.goodreads.com/book/show/3735293-clean-code", 21 | "alt": "Clean Code: A Handbook of Agile Software Craftsmanship" 22 | }, 23 | { 24 | "id": 3, 25 | "created_at": "0001-01-01T00:00:00Z", 26 | "updated_at": "0001-01-01T00:00:00Z", 27 | "deleted_at": null, 28 | "title": "Software Craftsmanship", 29 | "src": "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1370897661i/18054154.jpg", 30 | "url": "https://www.goodreads.com/book/show/18054154-software-craftsmanship", 31 | "alt": "The Software Craftsman: Professionalism, Pragmatism, Pride" 32 | }, 33 | { 34 | "id": 4, 35 | "created_at": "0001-01-01T00:00:00Z", 36 | "updated_at": "0001-01-01T00:00:00Z", 37 | "deleted_at": null, 38 | "title": "Extreme Programming Explained", 39 | "src": "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1386925310i/67833.jpg", 40 | "url": "https://www.goodreads.com/book/show/67833.Extreme_Programming_Explained", 41 | "alt": "Extreme Programming Explained: Embrace Change (The XP Series)" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/post_t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "created_at": "0001-01-01T00:00:00Z", 4 | "updated_at": "0001-01-01T00:00:00Z", 5 | "deleted_at": null, 6 | "title": "First Blog Post", 7 | "text": "This is blog about something.", 8 | "author": "Martin", 9 | "next": null, 10 | "next_post_id": 2, 11 | "previous": null, 12 | "previous_post_id": 0, 13 | "posted_on": "2018-08-24T14:00:00Z", 14 | "sections": [ 15 | { 16 | "id": 1, 17 | "created_at": "0001-01-01T00:00:00Z", 18 | "updated_at": "0001-01-01T00:00:00Z", 19 | "deleted_at": null, 20 | "post_id": 1, 21 | "name": "Title" 22 | }, 23 | { 24 | "id": 2, 25 | "created_at": "0001-01-01T00:00:00Z", 26 | "updated_at": "0001-01-01T00:00:00Z", 27 | "deleted_at": null, 28 | "post_id": 1, 29 | "name": "Subsection" 30 | } 31 | ], 32 | "tags": [ 33 | { 34 | "id": 1, 35 | "created_at": "0001-01-01T00:00:00Z", 36 | "updated_at": "0001-01-01T00:00:00Z", 37 | "deleted_at": null, 38 | "post_id": 1, 39 | "project_id": 0, 40 | "name": "Python" 41 | }, 42 | { 43 | "id": 3, 44 | "created_at": "0001-01-01T00:00:00Z", 45 | "updated_at": "0001-01-01T00:00:00Z", 46 | "deleted_at": null, 47 | "post_id": 1, 48 | "project_id": 0, 49 | "name": "Crypto" 50 | }, 51 | { 52 | "id": 4, 53 | "created_at": "0001-01-01T00:00:00Z", 54 | "updated_at": "0001-01-01T00:00:00Z", 55 | "deleted_at": null, 56 | "post_id": 1, 57 | "project_id": 0, 58 | "name": "Golang" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/post_t3.json: -------------------------------------------------------------------------------- 1 | { 2 | "posts": [ 3 | { 4 | "id": 3, 5 | "created_at": "0001-01-01T00:00:00Z", 6 | "updated_at": "0001-01-01T00:00:00Z", 7 | "deleted_at": null, 8 | "title": "3rd Blog Post", 9 | "text": "Another dummy content", 10 | "author": "Martin", 11 | "next": null, 12 | "next_post_id": 0, 13 | "previous": null, 14 | "previous_post_id": 2, 15 | "posted_on": "2019-05-30T19:00:00Z", 16 | "sections": null, 17 | "tags": null 18 | }, 19 | { 20 | "id": 2, 21 | "created_at": "0001-01-01T00:00:00Z", 22 | "updated_at": "0001-01-01T00:00:00Z", 23 | "deleted_at": null, 24 | "title": "Second Blog Post", 25 | "text": "This is blog about something else...", 26 | "author": "Martin", 27 | "next": null, 28 | "next_post_id": 3, 29 | "previous": null, 30 | "previous_post_id": 1, 31 | "posted_on": "2019-02-24T13:00:00Z", 32 | "sections": null, 33 | "tags": null 34 | }, 35 | { 36 | "id": 1, 37 | "created_at": "0001-01-01T00:00:00Z", 38 | "updated_at": "0001-01-01T00:00:00Z", 39 | "deleted_at": null, 40 | "title": "First Blog Post", 41 | "text": "This is blog about something.", 42 | "author": "Martin", 43 | "next": null, 44 | "next_post_id": 2, 45 | "previous": null, 46 | "previous_post_id": 0, 47 | "posted_on": "2018-08-24T14:00:00Z", 48 | "sections": null, 49 | "tags": null 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/project_t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "id": 1, 5 | "created_at": "0001-01-01T00:00:00Z", 6 | "updated_at": "0001-01-01T00:00:00Z", 7 | "deleted_at": null, 8 | "name": "IoT Cloud", 9 | "src": "https://via.placeholder.com/150", 10 | "url": "https://github.com/MartinHeinz/IoT-Cloud", 11 | "description": "Cloud framework for IoT (Internet of Things), which focuses on security and privacy of its users, their devices and data", 12 | "tags": [ 13 | { 14 | "id": 6, 15 | "created_at": "0001-01-01T00:00:00Z", 16 | "updated_at": "0001-01-01T00:00:00Z", 17 | "deleted_at": null, 18 | "post_id": 0, 19 | "project_id": 1, 20 | "name": "Python" 21 | }, 22 | { 23 | "id": 7, 24 | "created_at": "0001-01-01T00:00:00Z", 25 | "updated_at": "0001-01-01T00:00:00Z", 26 | "deleted_at": null, 27 | "post_id": 0, 28 | "project_id": 1, 29 | "name": "Cryptography" 30 | }, 31 | { 32 | "id": 8, 33 | "created_at": "0001-01-01T00:00:00Z", 34 | "updated_at": "0001-01-01T00:00:00Z", 35 | "deleted_at": null, 36 | "post_id": 0, 37 | "project_id": 1, 38 | "name": "Privacy" 39 | }, 40 | { 41 | "id": 9, 42 | "created_at": "0001-01-01T00:00:00Z", 43 | "updated_at": "0001-01-01T00:00:00Z", 44 | "deleted_at": null, 45 | "post_id": 0, 46 | "project_id": 1, 47 | "name": "IoT" 48 | } 49 | ] 50 | }, 51 | { 52 | "id": 2, 53 | "created_at": "0001-01-01T00:00:00Z", 54 | "updated_at": "0001-01-01T00:00:00Z", 55 | "deleted_at": null, 56 | "name": "Blog & Personal Website", 57 | "src": "https://via.placeholder.com/150", 58 | "url": "https://github.com/MartinHeinz/blog-backend", 59 | "description": "This website. Goal of this project was to learn Go and Vue.js and as a byproduct I created personal website and blog.", 60 | "tags": [ 61 | { 62 | "id": 10, 63 | "created_at": "0001-01-01T00:00:00Z", 64 | "updated_at": "0001-01-01T00:00:00Z", 65 | "deleted_at": null, 66 | "post_id": 0, 67 | "project_id": 2, 68 | "name": "Vue" 69 | }, 70 | { 71 | "id": 11, 72 | "created_at": "0001-01-01T00:00:00Z", 73 | "updated_at": "0001-01-01T00:00:00Z", 74 | "deleted_at": null, 75 | "post_id": 0, 76 | "project_id": 2, 77 | "name": "Golang" 78 | }, 79 | { 80 | "id": 12, 81 | "created_at": "0001-01-01T00:00:00Z", 82 | "updated_at": "0001-01-01T00:00:00Z", 83 | "deleted_at": null, 84 | "post_id": 0, 85 | "project_id": 2, 86 | "name": "Docker" 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/section_t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": [ 3 | { 4 | "id": 1, 5 | "created_at": "0001-01-01T00:00:00Z", 6 | "updated_at": "0001-01-01T00:00:00Z", 7 | "deleted_at": null, 8 | "post_id": 1, 9 | "name": "Title" 10 | }, 11 | { 12 | "id": 2, 13 | "created_at": "0001-01-01T00:00:00Z", 14 | "updated_at": "0001-01-01T00:00:00Z", 15 | "deleted_at": null, 16 | "post_id": 1, 17 | "name": "Subsection" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cmd/blog_backend/test_data/test_case_data/tag_t1.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": [ 3 | { 4 | "id": 1, 5 | "created_at": "0001-01-01T00:00:00Z", 6 | "updated_at": "0001-01-01T00:00:00Z", 7 | "deleted_at": null, 8 | "post_id": 1, 9 | "project_id": 0, 10 | "name": "Python" 11 | }, 12 | { 13 | "id": 3, 14 | "created_at": "0001-01-01T00:00:00Z", 15 | "updated_at": "0001-01-01T00:00:00Z", 16 | "deleted_at": null, 17 | "post_id": 1, 18 | "project_id": 0, 19 | "name": "Crypto" 20 | }, 21 | { 22 | "id": 4, 23 | "created_at": "0001-01-01T00:00:00Z", 24 | "updated_at": "0001-01-01T00:00:00Z", 25 | "deleted_at": null, 26 | "post_id": 1, 27 | "project_id": 0, 28 | "name": "Golang" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /config/errors.yaml: -------------------------------------------------------------------------------- 1 | INTERNAL_SERVER_ERROR: 2 | message: "We have encountered an internal server error." 3 | developer_message: "Internal server error: {error}" 4 | 5 | NOT_FOUND: 6 | message: "{resource} was not found." 7 | 8 | UNAUTHORIZED: 9 | message: "Authentication failed." 10 | developer_message: "Authentication failed: {error}" 11 | 12 | INVALID_DATA: 13 | message: "There is some problem with the data you submitted. See \"details\" for more information." 14 | -------------------------------------------------------------------------------- /config/server.yaml: -------------------------------------------------------------------------------- 1 | # The Data Source Name for the database 2 | # Make sure you override this in production with the environment variable: RESTFUL_DSN 3 | dsn: "postgres://postgres:postgres@db:5432/blog?sslmode=disable" 4 | # Overridden in production with BACKEND_API_KEY 5 | api_key: api-key-for-testing 6 | # These are secret keys used for JWT signing and verification. 7 | # Make sure you override these keys in production by the following environment variables: 8 | # RESTFUL_JWT_VERIFICATION_KEY 9 | # RESTFUL_JWT_SIGNING_KEY 10 | jwt_verification_key: "QfCAH04Cob7b71QCqy738vw5XGSnFZ9d" 11 | jwt_signing_key: "QfCAH04Cob7b71QCqy738vw5XGSnFZ9d" 12 | # Uncomment the following line and set an appropriate JWT signing method, if needed 13 | # The default signing method is HS256. 14 | #jwt_signing_method: "HS256" 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | backend: 4 | image: martinheinz/blog_backend:latest 5 | container_name: blog_backend 6 | healthcheck: 7 | test: ["CMD", "curl", "-f", "backend"] 8 | interval: 30s 9 | timeout: 10s 10 | retries: 5 11 | depends_on: 12 | - blog_db 13 | ports: 14 | - 8080:8080 15 | volumes: 16 | - ./data/certbot/conf/live/${HOST}:/etc/certs 17 | 18 | blog_db: 19 | image: martinheinz/blog_db:latest 20 | build: 21 | context: ./postgres 22 | dockerfile: ./Dockerfile 23 | container_name: db 24 | healthcheck: 25 | test: ["CMD-SHELL", "pg_isready -U postgres"] 26 | interval: 10s 27 | timeout: 5s 28 | retries: 5 29 | volumes: 30 | - data:/var/lib/postgresql/data 31 | expose: 32 | - 5432 33 | ports: 34 | - 5431:5432 35 | environment: 36 | POSTGRES_USER: "postgres" 37 | POSTGRES_PASSWORD: "postgres" 38 | DB_NAME: "blog" 39 | POSTGRES_DB: blog 40 | PGPORT: 5432 41 | POPULATE_DB: ${POPULATE_DB} 42 | 43 | volumes: 44 | data: {} 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MartinHeinz/blog-backend 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/anargu/gin-brotli v0.0.0-20210111213823-a283cb19bf94 7 | github.com/gin-gonic/gin v1.6.3 8 | github.com/jinzhu/gorm v1.9.8 9 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 10 | github.com/modern-go/reflect2 v1.0.1 // indirect 11 | github.com/spf13/viper v1.4.0 12 | github.com/stretchr/testify v1.4.0 13 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // VERSION is the app-global version string, which should be substituted with a 4 | // real value during build. 5 | var VERSION = "UNKNOWN" 6 | -------------------------------------------------------------------------------- /postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11 2 | 3 | # Custom initialization scripts 4 | COPY ./create_db.sh /docker-entrypoint-initdb.d/20-create_db.sh 5 | COPY schema.sql /schema.sql 6 | COPY test_data.sql /test_data.sql 7 | 8 | RUN chmod +x /docker-entrypoint-initdb.d/20-create_db.sh 9 | -------------------------------------------------------------------------------- /postgres/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## PostgreSQL Database Image 3 | 4 | _Notes:_ 5 | 6 | * Use `POPULATE_DB` variable in `.env` file to change whether database is populated with data or not 7 | 8 | --------------------------------- 9 | 10 | * When repopulating database make sure to delete `go-vue-blog_data` volume otherwise updates of schema or test data won't 11 | take effect. 12 | 13 | --------------------------------- 14 | 15 | _Repopulating steps:_ 16 | 17 | ``` 18 | docker-compose rm # Remove old containers... 19 | ``` 20 | _Set `.env` variable to `1` or `0`._ 21 | ``` 22 | docker-compose build # Builds ONLY blog_db image 23 | docker-compose up blog_db # Start only blog_db to create schema and optionally populate with test data 24 | ``` 25 | _Stop docker-compose (CTRL-C)_ 26 | ``` 27 | docker-compose up # Start all services this time 28 | curl https://localhost:8080/api/v1// | jq # Check presence of populated data... 29 | ``` 30 | -------------------------------------------------------------------------------- /postgres/create_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | POSTGRES="psql --username ${POSTGRES_USER}" 5 | 6 | echo "Creating database: ${DB_NAME}" 7 | 8 | $POSTGRES < /bin/bash 248 | -- pg_dump -s blog -Upostgres | sed -e '/^--/d' 249 | -- 250 | -- Don't forget to remove automigrate 251 | -------------------------------------------------------------------------------- /postgres/test_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('First Blog Post', 'This is blog about something.', 'Martin', '2018-08-24 14:00:00', null, null); 2 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('Second Blog Post', 'This is blog about something else...', 'Martin', '2019-02-24 13:00:00', null, null); 3 | INSERT INTO posts (title, text, author, posted_on, next_post_id, previous_post_id) VALUES ('3rd Blog Post', 'Another dummy content', 'Martin', '2019-05-30 19:00:00', null, null); 4 | 5 | UPDATE posts SET next_post_id = 2, previous_post_id = null WHERE id = 1; 6 | UPDATE posts SET next_post_id = 3, previous_post_id = 1 WHERE id = 2; 7 | UPDATE posts SET next_post_id = null, previous_post_id = 2 WHERE id = 3; 8 | 9 | INSERT INTO sections (name, post_id) VALUES ('Title', 1); 10 | INSERT INTO sections (name, post_id) VALUES ('Subsection', 1); 11 | INSERT INTO sections (name, post_id) VALUES ('Intro', 2); 12 | INSERT INTO sections (name, post_id) VALUES ('Subsection', 2); 13 | INSERT INTO sections (name, post_id) VALUES ('Subsection 2', 2); 14 | INSERT INTO sections (name, post_id) VALUES ('Subsection 3', 2); 15 | INSERT INTO sections (name, post_id) VALUES ('Intro', 3); 16 | INSERT INTO sections (name, post_id) VALUES ('First Section', 3); 17 | 18 | INSERT INTO projects (name, thumbnail_url, url, description) VALUES ('IoT Cloud', 'https://via.placeholder.com/150', 'https://github.com/MartinHeinz/IoT-Cloud', 'Cloud framework for IoT (Internet of Things), which focuses on security and privacy of its users, their devices and data'); 19 | INSERT INTO projects (name, thumbnail_url, url, description) VALUES ('Blog & Personal Website', 'https://via.placeholder.com/150', 'https://github.com/MartinHeinz/blog-backend', 'This website. Goal of this project was to learn Go and Vue.js and as a byproduct I created personal website and blog.'); 20 | 21 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', 1, null); 22 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', 2, null); 23 | INSERT INTO tags (name, post_id, project_id) VALUES ('Crypto', 1, null); 24 | INSERT INTO tags (name, post_id, project_id) VALUES ('Golang', 1, null); 25 | INSERT INTO tags (name, post_id, project_id) VALUES ('Vue', 3, null); 26 | 27 | INSERT INTO tags (name, post_id, project_id) VALUES ('Python', null, 1); 28 | INSERT INTO tags (name, post_id, project_id) VALUES ('Cryptography', null, 1); 29 | INSERT INTO tags (name, post_id, project_id) VALUES ('Privacy', null, 1); 30 | INSERT INTO tags (name, post_id, project_id) VALUES ('IoT', null, 1); 31 | INSERT INTO tags (name, post_id, project_id) VALUES ('Vue', null, 2); 32 | INSERT INTO tags (name, post_id, project_id) VALUES ('Golang', null, 2); 33 | INSERT INTO tags (name, post_id, project_id) VALUES ('Docker', null, 2); 34 | 35 | INSERT INTO books (title, cover_url, url, alt) VALUES ('The Go Programming Language', 'https://www.gopl.io/cover.png', 'https://www.gopl.io/', 'The Go Programming Language'); 36 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Clean Code', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1436202607i/3735293._SX318_.jpg', 'https://www.goodreads.com/book/show/3735293-clean-code', 'Clean Code: A Handbook of Agile Software Craftsmanship'); 37 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Software Craftsmanship', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1370897661i/18054154.jpg', 'https://www.goodreads.com/book/show/18054154-software-craftsmanship', 'The Software Craftsman: Professionalism, Pragmatism, Pride'); 38 | INSERT INTO books (title, cover_url, url, alt) VALUES ('Extreme Programming Explained', 'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1386925310i/67833.jpg', 'https://www.goodreads.com/book/show/67833.Extreme_Programming_Explained', 'Extreme Programming Explained: Embrace Change (The XP Series)'); 39 | -------------------------------------------------------------------------------- /reports.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mkdir -p reports 4 | touch reports/coverage.out reports/test-report.out reports/vet.out 5 | touch c.out 6 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=MartinHeinz_blog-backend 2 | sonar.organization=martinheinz-github 3 | sonar.projectName=blog-backend 4 | sonar.projectVersion=0.0.1 5 | 6 | sonar.login=b806fa3a541f3d3cf15922c6cb13d67429423a00 7 | sonar.password= 8 | 9 | sonar.links.homepage=https://github.com/MartinHeinz/blog-backend 10 | sonar.links.ci=https://travis-ci.com/MartinHeinz/blog-backend 11 | sonar.links.scm=https://github.com/MartinHeinz/blog-backend 12 | 13 | sonar.sources=cmd 14 | sonar.tests=cmd 15 | sonar.binaries=cmd/bin/linux_amd64 16 | sonar.exclusions=**/vendor/** 17 | sonar.test.exclusions=**/*.go 18 | sonar.test.inclusions=**/*_test.go 19 | 20 | sonar.go.coverage.reportPath=reports/coverage.out 21 | sonar.go.tests.reportPaths=reports/test-report.out 22 | sonar.go.govet.reportPaths=reports/vet.out 23 | --------------------------------------------------------------------------------