diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f46a74b..96bdb47 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,34 +1,26 @@ name: Main -on: [push, pull_request] +on: + push: + branches: + - '**' + tags: + - '**' + - '!v[0-9]+.[0-9]+.[0-9]+' + pull_request: jobs: - test_build_publish: - name: Test, build, and publish + test_build: + name: Test and build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - - name: Install dependencies - run: make setup - name: Export GOBIN uses: actions/setup-go@v2 with: go-version: 1.16 + - name: Install dependencies + run: make setup - name: Run tests run: make test - name: Build image run: make build_image - - name: Get Branch # Needed to evaluate env.BRANCH. - if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' }} # Otherwise will fail on pull requests. - run: | - raw=$(git branch -r --contains ${{ github.ref }}) - branch=${raw##*/} - echo "BRANCH=$branch" >> $GITHUB_ENV - - name: Login to Docker Hub - if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' && env.BRANCH == 'master' }} # Only login for tagged commits pushed to master. - uses: docker/login-action@v1 - with: # Secrets are not exposed to pull request contexts. - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Publish image - if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' && env.BRANCH == 'master' }} # Only publish for tagged commits pushed to master. - run: make push_image diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f41aa31 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' +jobs: + test_build_publish: + name: Test, build, and publish + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Export GOBIN + uses: actions/setup-go@v2 + with: + go-version: 1.16 + - name: Install dependencies + run: make setup + - name: Run tests + run: make test + - name: Build image + run: make build_image + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Publish image + run: make push_image diff --git a/Makefile b/Makefile index a45cbe8..7bfcaa2 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,8 @@ test: .PHONY: setup setup: - go get -u github.com/fzipp/gocyclo/cmd/gocyclo - go get -u honnef.co/go/tools/cmd/staticcheck + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + go install honnef.co/go/tools/cmd/staticcheck@latest .PHONY: build_image build_image: diff --git a/README.md b/README.md index 48b7570..e2fc192 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,14 @@ can be reused. ### Why Matrix instead of X? -I would totally do this with Signal if there was a proper API. -Unfortunately, neither [Signal](https://signal.org/) nor [WhatsApp](https://www.whatsapp.com/) come with an API through which PushBits could interact. +This project totally would've used Signal if it would offer a proper API. +Sadly, neither [Signal](https://signal.org/) nor [WhatsApp](https://www.whatsapp.com/) come with an API (at the time of writing) through which PushBits could interact. In [Telegram](https://telegram.org/) there is an API to run bots, but these are limited in that they cannot create chats by themselves. If you insist on going with Telegram, have a look at [webhook2telegram](https://github.com/muety/webhook2telegram). -I myself started using Matrix only for this project. The idea of a federated, synchronized but yet end-to-end encrypted protocol is awesome, but its clients simply aren't really there yet. -Still, if you haven't tried it yet, I suggest you to check it out. +Still, if you haven't tried it yet, we'd encourage you to check it out. ## 🤘 Features @@ -98,12 +97,12 @@ The SQLite database would be written to `./data/pushbits.db`. ## 📄 Usage Now, how can you interact with the server? -I wrote [a little CLI tool called pbcli](https://github.com/PushBits/cli) to make basic API requests to the server. +We provide [a little CLI tool called pbcli](https://github.com/PushBits/cli) to make basic API requests to the server. It helps you to create new users and applications. You will find further instructions in the linked repository. -At the time of writing, there is no fancy GUI built-in, and I'm not sure if this is necessary at all. -I don't do much front end development myself, so if you want to contribute in this regard I'm happy if you reach out! +At the time of writing, there is no fancy GUI built-in, and we're not sure if this is necessary at all. +Currently, we would like to avoid front end development, so if you want to contribute in this regard we're happy if you reach out! After you have created a user and an application, you can use the API to send a push notification to your Matrix account. @@ -124,13 +123,13 @@ pbcli application show $PB_APPLICATION --url https://pushbits.example.com --user ### Message options -Messages are supporting three different syntaxes: +Messages can be specified in three different syntaxes: -* text/plain -* text/html -* text/markdown +* `text/plain` +* `text/html` +* `text/markdown` -To set a specific syntax you need to set the `extras` ([inspired by Gotifys message extras](https://gotify.net/docs/msgextras#clientdisplay)): +To set a specific syntax you need to set the `extras` parameter ([inspired by Gotify's message extras](https://gotify.net/docs/msgextras#clientdisplay)): ```bash curl \ @@ -140,13 +139,16 @@ curl \ "https://pushbits.example.com/message?token=$PB_TOKEN" ``` -HTML-Content might not be fully rendered in your Matrix-Client - see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes). This also holds for Markdown, as it is transfered to the corresponding HTML-syntax. +HTML content might not be fully rendered in your Matrix client; see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes). +This also holds for Markdown, as it is translated into the corresponding HTML syntax. ### Deleting a Message -You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted". +You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted". -You need the message ID for deleting a message. As it might contain characters not valid in uris we provide an additional `id_url_encoded` field for messages, use that value for deleting a message. +To delete a message, you need its message ID which is provided as part of the response when you send the message. +The ID might contain characters not valid in URIs. +We hence provide an additional `id_url_encoded` field for messages; you can directly use it when deleting a message without performing encoding yourself. ```bash curl \ @@ -169,3 +171,34 @@ git clone https://github.com/pushbits/server.git ``` [![Stargazers over time](https://starchart.cc/pushbits/server.svg)](https://starchart.cc/pushbits/server) + +### Testing + +Testing is essential for delivering good and reliable software. +PushBits uses Go's integrated test features. +Unfortunately, writing tests is quite time consuming and therefore not every feature and every line of code is automatically tested. +Feel free to help us improve our tests. + +To run tests for a single (sub)module you can simply execute the following command in the module's folder. + +```bash +go test +``` + +To get the testing coverage for a module use the `-cover` flag. + +```bash +go test -cover +``` + +To execute a single test use the `-run` flag. + +```bash +go test -run "TestApi_getUser" +``` + +Running tests for all PushBits module is done like this: + +```bash +make test +``` diff --git a/config.example.yml b/config.example.yml index 7d042d1..34457c5 100644 --- a/config.example.yml +++ b/config.example.yml @@ -57,6 +57,6 @@ crypto: saltlength: 16 keylength: 32 -formatting: +formatting: # Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red). coloredtitle: false diff --git a/go.mod b/go.mod index 28b463c..6f611b3 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/pushbits/server -go 1.14 +go 1.16 require ( github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b - github.com/fzipp/gocyclo v0.3.1 // indirect github.com/gin-contrib/location v0.0.2 github.com/gin-gonic/gin v1.6.3 github.com/go-playground/validator/v10 v10.3.0 // indirect @@ -18,12 +17,12 @@ require ( github.com/mattn/go-sqlite3 v1.14.6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/stretchr/testify v1.7.0 github.com/ugorji/go v1.2.4 // indirect golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect - golang.org/x/tools v0.1.3 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v2 v2.4.0 gorm.io/driver/mysql v1.0.4 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.20.12 - honnef.co/go/tools v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 35c2598..3a1ffae 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b/go.mod h1:Kmn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc= -github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4= github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -19,13 +17,11 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -48,11 +44,9 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= @@ -63,11 +57,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -75,64 +67,30 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8= github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -146,7 +104,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -159,5 +116,3 @@ gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9D gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -honnef.co/go/tools v0.2.0 h1:ws8AfbgTX3oIczLPNPCu5166oBg9ST2vNs0rcht+mDE= -honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= diff --git a/internal/api/application.go b/internal/api/application.go index c598414..f90c9bd 100644 --- a/internal/api/application.go +++ b/internal/api/application.go @@ -114,6 +114,7 @@ func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) { var createApplication model.CreateApplication if err := ctx.Bind(&createApplication); err != nil { + log.Println(err) return } diff --git a/internal/api/application_test.go b/internal/api/application_test.go new file mode 100644 index 0000000..ad2ba69 --- /dev/null +++ b/internal/api/application_test.go @@ -0,0 +1,348 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "testing" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/database" + "github.com/pushbits/server/internal/model" + "github.com/pushbits/server/tests" + "github.com/pushbits/server/tests/mockups" + "github.com/stretchr/testify/assert" +) + +var TestApplicationHandler *ApplicationHandler +var TestUsers []*model.User +var TestDatabase *database.Database + +// Collect all created applications to check & delete them later +var SuccessAplications map[uint][]model.Application + +func TestMain(m *testing.M) { + // Get main config and adapt + config := &configuration.Configuration{} + + config.Database.Connection = "pushbits-test.db" + config.Database.Dialect = "sqlite3" + config.Crypto.Argon2.Iterations = 4 + config.Crypto.Argon2.Parallelism = 4 + config.Crypto.Argon2.Memory = 131072 + config.Crypto.Argon2.SaltLength = 16 + config.Crypto.Argon2.KeyLength = 32 + + // Set up test environment + db, err := mockups.GetEmptyDatabase(config.Crypto) + if err != nil { + cleanUp() + log.Println("Can not set up database: ", err) + os.Exit(1) + } + TestDatabase = db + + appHandler, err := getApplicationHandler(config) + if err != nil { + cleanUp() + log.Println("Can not set up application handler: ", err) + os.Exit(1) + } + + TestApplicationHandler = appHandler + TestUsers = mockups.GetUsers(config) + SuccessAplications = make(map[uint][]model.Application) + + // Run + m.Run() + cleanUp() +} + +func TestApi_RegisterApplicationWithoutUser(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + reqWoUser := tests.Request{Name: "Invalid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test1", "strict_compatibility": true}`, Headers: map[string]string{"Content-Type": "application/json"}} + _, c, err := reqWoUser.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + assert.Panicsf(func() { TestApplicationHandler.CreateApplication(c) }, "CreateApplication did not panic altough user is not in context") + +} + +func TestApi_RegisterApplication(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + testCases := make([]tests.Request, 0) + testCases = append(testCases, tests.Request{Name: "Invalid Form Data", Method: "POST", Endpoint: "/application", Data: "k=1&v=abc", ShouldStatus: 400}) + testCases = append(testCases, tests.Request{Name: "Invalid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test1", "strict_compatibility": "oh yes"}`, Headers: map[string]string{"Content-Type": "application/json"}, ShouldStatus: 400}) + testCases = append(testCases, tests.Request{Name: "Valid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test2", "strict_compatibility": true}`, Headers: map[string]string{"Content-Type": "application/json"}, ShouldStatus: 200}) + + for _, user := range TestUsers { + SuccessAplications[user.ID] = make([]model.Application, 0) + for _, req := range testCases { + var application model.Application + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + TestApplicationHandler.CreateApplication(c) + + // Parse body only for successful requests + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + body, err := ioutil.ReadAll(w.Body) + assert.NoErrorf(err, "Can not read request body") + if err != nil { + continue + } + err = json.Unmarshal(body, &application) + assert.NoErrorf(err, "Can not unmarshal request body") + if err != nil { + continue + } + + SuccessAplications[user.ID] = append(SuccessAplications[user.ID], application) + } + + assert.Equalf(w.Code, req.ShouldStatus, "CreateApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +func TestApi_GetApplications(t *testing.T) { + var applications []model.Application + + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + testCases := make([]tests.Request, 0) + testCases = append(testCases, tests.Request{Name: "Valid Request", Method: "GET", Endpoint: "/application", ShouldStatus: 200}) + + for _, user := range TestUsers { + for _, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + TestApplicationHandler.GetApplications(c) + + // Parse body only for successful requests + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + body, err := ioutil.ReadAll(w.Body) + assert.NoErrorf(err, "Can not read request body") + if err != nil { + continue + } + err = json.Unmarshal(body, &applications) + assert.NoErrorf(err, "Can not unmarshal request body") + if err != nil { + continue + } + + assert.Truef(validateAllApplications(user, applications), "Did not find application created previously") + assert.Equalf(len(applications), len(SuccessAplications[user.ID]), "Created %d application(s) but got %d back", len(SuccessAplications[user.ID]), len(applications)) + } + + assert.Equalf(w.Code, req.ShouldStatus, "GetApplications (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +func TestApi_GetApplicationsWithoutUser(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + testCase := tests.Request{Name: "Valid Request", Method: "GET", Endpoint: "/application"} + + _, c, err := testCase.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + assert.Panicsf(func() { TestApplicationHandler.GetApplications(c) }, "GetApplications did not panic altough user is not in context") + +} + +func TestApi_GetApplicationErrors(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + // Arbitrary test cases + testCases := make(map[uint]tests.Request) + testCases[0] = tests.Request{Name: "Requesting unknown application 0", Method: "GET", Endpoint: "/application/0", ShouldStatus: 404} + testCases[5555] = tests.Request{Name: "Requesting unknown application 5555", Method: "GET", Endpoint: "/application/5555", ShouldStatus: 404} + testCases[99999999999999999] = tests.Request{Name: "Requesting unknown application 99999999999999999", Method: "GET", Endpoint: "/application/99999999999999999", ShouldStatus: 404} + + for _, user := range TestUsers { + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + c.Set("id", id) + TestApplicationHandler.GetApplication(c) + + assert.Equalf(w.Code, req.ShouldStatus, "GetApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +func TestApi_GetApplication(t *testing.T) { + var application model.Application + + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + // Previously generated applications + for _, user := range TestUsers { + for _, app := range SuccessAplications[user.ID] { + req := tests.Request{Name: fmt.Sprintf("Requesting application %s (%d)", app.Name, app.ID), Method: "GET", Endpoint: fmt.Sprintf("/application/%d", app.ID), ShouldStatus: 200} + + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + c.Set("id", app.ID) + TestApplicationHandler.GetApplication(c) + + // Parse body only for successful requests + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + body, err := ioutil.ReadAll(w.Body) + assert.NoErrorf(err, "Can not read request body") + if err != nil { + continue + } + err = json.Unmarshal(body, &application) + assert.NoErrorf(err, "Can not unmarshal request body: %v", err) + if err != nil { + continue + } + + assert.Equalf(application.ID, app.ID, "Application ID should be %d but is %d", app.ID, application.ID) + assert.Equalf(application.Name, app.Name, "Application Name should be %s but is %s", app.Name, application.Name) + assert.Equalf(application.UserID, app.UserID, "Application user ID should be %d but is %d", app.UserID, application.UserID) + + } + + assert.Equalf(w.Code, req.ShouldStatus, "GetApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +func TestApi_UpdateApplication(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + for _, user := range TestUsers { + testCases := make(map[uint]tests.Request) + // Previously generated applications + for _, app := range SuccessAplications[user.ID] { + newName := app.Name + "-new_name" + updateApp := model.UpdateApplication{ + Name: &newName, + } + updateAppBytes, err := json.Marshal(updateApp) + assert.NoErrorf(err, "Error on marshaling updateApplication struct") + + // Valid + testCases[app.ID] = tests.Request{Name: fmt.Sprintf("Update application (valid) %s (%d)", app.Name, app.ID), Method: "PUT", Endpoint: fmt.Sprintf("/application/%d", app.ID), ShouldStatus: 200, Data: string(updateAppBytes), Headers: map[string]string{"Content-Type": "application/json"}} + // Invalid + testCases[app.ID] = tests.Request{Name: fmt.Sprintf("Update application (invalid) %s (%d)", app.Name, app.ID), Method: "PUT", Endpoint: fmt.Sprintf("/application/%d", app.ID), ShouldStatus: 200, Data: "{}", Headers: map[string]string{"Content-Type": "application/json"}} + } + // Arbitrary test cases + testCases[5555] = tests.Request{Name: "Update application 5555", Method: "PUT", Endpoint: "/application/5555", ShouldStatus: 404, Data: "random data"} + testCases[5556] = tests.Request{Name: "Update application 5556", Method: "PUT", Endpoint: "/application/5556", ShouldStatus: 404, Data: `{"new_name": "new name"}`, Headers: map[string]string{"Content-Type": "application/json"}} + + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + c.Set("id", id) + TestApplicationHandler.UpdateApplication(c) + + assert.Equalf(w.Code, req.ShouldStatus, "UpdateApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +func TestApi_DeleteApplication(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + for _, user := range TestUsers { + testCases := make(map[uint]tests.Request) + // Previously generated applications + for _, app := range SuccessAplications[user.ID] { + testCases[app.ID] = tests.Request{Name: fmt.Sprintf("Delete application %s (%d)", app.Name, app.ID), Method: "DELETE", Endpoint: fmt.Sprintf("/application/%d", app.ID), ShouldStatus: 200} + } + // Arbitrary test cases + testCases[5555] = tests.Request{Name: "Delete application 5555", Method: "DELETE", Endpoint: "/application/5555", ShouldStatus: 404} + + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + c.Set("id", id) + TestApplicationHandler.DeleteApplication(c) + + assert.Equalf(w.Code, req.ShouldStatus, "DeleteApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, req.ShouldStatus, w.Code) + } + } +} + +// GetApplicationHandler creates and returns an application handler +func getApplicationHandler(c *configuration.Configuration) (*ApplicationHandler, error) { + dispatcher := &mockups.MockDispatcher{} + + return &ApplicationHandler{ + DB: TestDatabase, + DP: dispatcher, + }, nil +} + +// True if all created applications are in list +func validateAllApplications(user *model.User, apps []model.Application) bool { + if _, ok := SuccessAplications[user.ID]; !ok { + return len(apps) == 0 + } + + for _, successApp := range SuccessAplications[user.ID] { + foundApp := false + for _, app := range apps { + if app.ID == successApp.ID { + foundApp = true + break + } + } + + if !foundApp { + return false + } + } + + return true +} + +func cleanUp() { + os.Remove("pushbits-test.db") +} diff --git a/internal/api/context_test.go b/internal/api/context_test.go new file mode 100644 index 0000000..f3bcdf8 --- /dev/null +++ b/internal/api/context_test.go @@ -0,0 +1,117 @@ +package api + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/model" + "github.com/pushbits/server/tests" + "github.com/pushbits/server/tests/mockups" + "github.com/stretchr/testify/assert" +) + +func TestApi_getID(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + testValue := uint(1337) + + testCases := make(map[interface{}]tests.Request) + testCases[-1] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + testCases[uint(1)] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + testCases[uint(0)] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + testCases[uint(500)] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + testCases[500] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + testCases["test"] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + testCases[model.Application{}] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + testCases[&model.Application{}] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + testCases[&testValue] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 500} + + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("id", id) + idReturned, err := getID(c) + + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + idUint, ok := id.(uint) + if ok { + assert.Equalf(idReturned, idUint, "getApi id was set to %d but result is %d", idUint, idReturned) + } + assert.NoErrorf(err, "getId with id %v (%t) returned an error altough it should not: %v", id, id, err) + } else { + assert.Errorf(err, "getId with id %v (%t) returned no error altough it should", id, id) + } + + assert.Equalf(w.Code, req.ShouldStatus, "getApi id was set to %v (%T) and should result in status code %d but code is %d", id, id, req.ShouldStatus, w.Code) + } +} + +func TestApi_getApplication(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + applications := mockups.GetAllApplications() + mockups.AddApplicationsToDb(TestDatabase, applications) + + // No testing of invalid ids as that is tested in TestApi_getID already + testCases := make(map[uint]tests.Request) + testCases[500] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 404} + testCases[1] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + testCases[2] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("id", id) + app, err := getApplication(c, TestDatabase) + + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + assert.Equalf(app.ID, id, "getApplication id was set to %d but resulting app id is %d", id, app.ID) + assert.NoErrorf(err, "getApplication with id %v (%t) returned an error altough it should not: %v", id, id, err) + } else { + assert.Errorf(err, "getApplication with id %v (%t) returned no error altough it should", id, id) + } + + assert.Equalf(w.Code, req.ShouldStatus, "getApplication id was set to %v (%T) and should result in status code %d but code is %d", id, id, req.ShouldStatus, w.Code) + + } +} + +func TestApi_getUser(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + _, err := mockups.AddUsersToDb(TestDatabase, TestUsers) + assert.NoErrorf(err, "Adding users to database failed: %v", err) + + // No testing of invalid ids as that is tested in TestApi_getID already + testCases := make(map[uint]tests.Request) + testCases[500] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 404} + testCases[1] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + testCases[2] = tests.Request{Name: "-", Method: "GET", Endpoint: "/", Data: "", ShouldStatus: 200} + + for id, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("id", id) + user, err := getUser(c, TestDatabase) + + if req.ShouldStatus >= 200 && req.ShouldStatus < 300 { + assert.Equalf(user.ID, id, "getUser id was set to %d but resulting app id is %d", id, user.ID) + assert.NoErrorf(err, "getUser with id %v (%t) returned an error altough it should not: %v", id, id, err) + } else { + assert.Errorf(err, "getUser with id %v (%t) returned no error altough it should", id, id) + } + + assert.Equalf(w.Code, req.ShouldStatus, "getUser id was set to %v (%T) and should result in status code %d but code is %d", id, id, req.ShouldStatus, w.Code) + } +} diff --git a/internal/api/errors.go b/internal/api/errors.go deleted file mode 100644 index 64c744a..0000000 --- a/internal/api/errors.go +++ /dev/null @@ -1,5 +0,0 @@ -package api - -import "errors" - -var ErrorMessageNotFound = errors.New("message not found") diff --git a/internal/api/util.go b/internal/api/util.go index d045139..ff9e481 100644 --- a/internal/api/util.go +++ b/internal/api/util.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/pushbits/server/internal/authentication" + "github.com/pushbits/server/internal/pberrors" "github.com/gin-gonic/gin" ) @@ -13,7 +14,7 @@ func successOrAbort(ctx *gin.Context, code int, err error) bool { if err != nil { // If we know the error force error code switch err { - case ErrorMessageNotFound: + case pberrors.ErrorMessageNotFound: ctx.AbortWithError(http.StatusNotFound, err) default: ctx.AbortWithError(code, err) diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index dc3dc9e..5da3653 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -4,6 +4,9 @@ import ( "github.com/jinzhu/configor" ) +// testMode indicates if the package is run in test mode +var testMode bool + // Argon2Config holds the parameters used for creating hashes with Argon2. type Argon2Config struct { Memory uint32 `default:"131072"` @@ -23,6 +26,13 @@ type Formatting struct { ColoredTitle bool `default:"false"` } +// Matrix holds credentials for a matrix account +type Matrix struct { + Homeserver string `default:"https://matrix.org"` + Username string `required:"true"` + Password string `required:"true"` +} + // Configuration holds values that can be configured by the user. type Configuration struct { Debug bool `default:"false"` @@ -39,11 +49,7 @@ type Configuration struct { Password string `default:"admin"` MatrixID string `required:"true"` } - Matrix struct { - Homeserver string `default:"https://matrix.org"` - Username string `required:"true"` - Password string `required:"true"` - } + Matrix Matrix Security struct { CheckHIBP bool `default:"false"` } @@ -52,6 +58,9 @@ type Configuration struct { } func configFiles() []string { + if testMode { + return []string{"config_unittest.yml"} + } return []string{"config.yml"} } diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go new file mode 100644 index 0000000..988661a --- /dev/null +++ b/internal/configuration/configuration_test.go @@ -0,0 +1,205 @@ +package configuration + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/jinzhu/configor" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +type Pair struct { + Is interface{} + Should interface{} +} + +func TestMain(m *testing.M) { + testMode = true + m.Run() + cleanUp() + os.Exit(0) +} + +func TestConfiguration_GetMinimal(t *testing.T) { + err := writeMinimalConfig() + if err != nil { + fmt.Println("Could not write minimal config: ", err) + os.Exit(1) + } + + validateConfig(t) +} + +func TestConfiguration_GetValid(t *testing.T) { + assert := assert.New(t) + + err := writeValidConfig() + if err != nil { + fmt.Println("Could not write valid config: ", err) + os.Exit(1) + } + + validateConfig(t) + + config := Get() + + expectedValues := make(map[string]Pair) + expectedValues["config.Admin.MatrixID"] = Pair{config.Admin.MatrixID, "000000"} + expectedValues["config.Matrix.Username"] = Pair{config.Matrix.Username, "default-username"} + expectedValues["config.Matrix.Password"] = Pair{config.Matrix.Password, "default-password"} + + for name, pair := range expectedValues { + assert.Equalf(pair.Is, pair.Should, fmt.Sprintf("%s should be %v but is %v", name, pair.Should, pair.Is)) + } +} + +func TestConfiguration_GetEmpty(t *testing.T) { + err := writeEmptyConfig() + if err != nil { + fmt.Println("Could not write empty config: ", err) + os.Exit(1) + } + + assert.Panicsf(t, func() { Get() }, "Get() did not panic altough config is empty") +} + +func TestConfiguration_GetInvalid(t *testing.T) { + err := writeInvalidConfig() + if err != nil { + fmt.Println("Could not write empty config: ", err) + os.Exit(1) + } + + assert.Panicsf(t, func() { Get() }, "Get() did not panic altough config is empty") +} + +func TestConfiguaration_ConfigFiles(t *testing.T) { + files := configFiles() + + assert.Greater(t, len(files), 0) + for _, file := range files { + assert.Truef(t, strings.HasSuffix(file, ".yml"), "%s is no yaml file", file) + } +} + +// Checks if the values in the configuration are plausible +func validateConfig(t *testing.T) { + assert := assert.New(t) + assert.NotPanicsf(func() { Get() }, "Get configuration should not panic") + + config := Get() + asGreater := make(map[string]Pair) + asGreater["config.Crypto.Argon2.Memory"] = Pair{config.Crypto.Argon2.Memory, uint32(0)} + asGreater["config.Crypto.Argon2.Iterations"] = Pair{config.Crypto.Argon2.Iterations, uint32(0)} + asGreater["config.Crypto.Argon2.SaltLength"] = Pair{config.Crypto.Argon2.SaltLength, uint32(0)} + asGreater["config.Crypto.Argon2.KeyLength"] = Pair{config.Crypto.Argon2.KeyLength, uint32(0)} + asGreater["config.Crypto.Argon2.Parallelism"] = Pair{config.Crypto.Argon2.Parallelism, uint8(0)} + asGreater["config.HTTP.Port"] = Pair{config.HTTP.Port, 0} + for name, pair := range asGreater { + assert.Greaterf(pair.Is, pair.Should, fmt.Sprintf("%s should be > %v but is %v", name, pair.Should, pair.Is)) + } + + asFalse := make(map[string]bool) + asFalse["config.Formatting.ColoredTitle"] = config.Formatting.ColoredTitle + asFalse["config.Debug"] = config.Debug + asFalse["config.Security.CheckHIBP"] = config.Security.CheckHIBP + for name, value := range asFalse { + assert.Falsef(value, fmt.Sprintf("%s should be false but is %t", name, value)) + } +} + +type MinimalConfiguration struct { + Admin struct { + MatrixID string + } + Matrix struct { + Username string + Password string + } +} + +type InvalidConfiguration struct { + Debug int + HTTP struct { + ListenAddress bool + } + Admin struct { + Name int + } + Formatting string +} + +// Writes a minimal config to config.yml +func writeMinimalConfig() error { + cleanUp() + config := MinimalConfiguration{} + config.Admin.MatrixID = "000000" + config.Matrix.Username = "default-username" + config.Matrix.Password = "default-password" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config_unittest.yml", configString, 0644) +} + +// Writes a config with default values to config.yml +func writeValidConfig() error { + cleanUp() + + // Load minimal config to get default values + writeMinimalConfig() + config := &Configuration{} + err := configor.New(&configor.Config{ + Environment: "production", + ENVPrefix: "PUSHBITS", + ErrorOnUnmatchedKeys: true, + }).Load(config, "config_unittest.yml") + if err != nil { + return err + } + + config.Admin.MatrixID = "000000" + config.Matrix.Username = "default-username" + config.Matrix.Password = "default-password" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config_unittest.yml", configString, 0644) +} + +// Writes a config that is empty +func writeEmptyConfig() error { + cleanUp() + return ioutil.WriteFile("config_unittest.yml", []byte(""), 0644) +} + +// Writes a config with invalid entries +func writeInvalidConfig() error { + cleanUp() + config := InvalidConfiguration{} + config.Debug = 1337 + config.HTTP.ListenAddress = true + config.Admin.Name = 23 + config.Formatting = "Nice" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config_unittest.yml", configString, 0644) +} + +func cleanUp() error { + return os.Remove("config_unittest.yml") +} diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 9fce7a0..67dcad9 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -8,8 +8,8 @@ import ( "github.com/gomarkdown/markdown" "github.com/matrix-org/gomatrix" - "github.com/pushbits/server/internal/api" "github.com/pushbits/server/internal/model" + "github.com/pushbits/server/internal/pberrors" ) // MessageFormat is a matrix message format @@ -77,7 +77,7 @@ func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNot if err != nil { log.Println(err) - return api.ErrorMessageNotFound + return pberrors.ErrorMessageNotFound } oldBody, oldFormattedBody, err = bodiesFromMessage(deleteMessage) @@ -182,7 +182,7 @@ func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event } start = messages.End } - return gomatrix.Event{}, api.ErrorMessageNotFound + return gomatrix.Event{}, pberrors.ErrorMessageNotFound } // Replaces the content of a matrix message @@ -254,19 +254,19 @@ func bodiesFromMessage(message gomatrix.Event) (body, formattedBody string, err body, ok := val.(string) if !ok { - return "", "", api.ErrorMessageNotFound + return "", "", pberrors.ErrorMessageNotFound } formattedBody = body } else { - return "", "", api.ErrorMessageNotFound + return "", "", pberrors.ErrorMessageNotFound } if val, ok := message.Content["formatted_body"]; ok { body, ok := val.(string) if !ok { - return "", "", api.ErrorMessageNotFound + return "", "", pberrors.ErrorMessageNotFound } formattedBody = body diff --git a/internal/pberrors/errors.go b/internal/pberrors/errors.go new file mode 100644 index 0000000..ff31d0c --- /dev/null +++ b/internal/pberrors/errors.go @@ -0,0 +1,6 @@ +package pberrors + +import "errors" + +// ErrorMessageNotFound indicates that a message does not exist +var ErrorMessageNotFound = errors.New("message not found") diff --git a/tests/mockups/application.go b/tests/mockups/application.go new file mode 100644 index 0000000..754b37e --- /dev/null +++ b/tests/mockups/application.go @@ -0,0 +1,32 @@ +package mockups + +import "github.com/pushbits/server/internal/model" + +// GetApplication1 returns an application with id 1 +func GetApplication1() *model.Application { + return &model.Application{ + ID: 1, + Token: "1234567890abcdefghijklmn", + UserID: 1, + Name: "App1", + } +} + +// GetApplication2 returns an application with id 2 +func GetApplication2() *model.Application { + return &model.Application{ + ID: 2, + Token: "0987654321xyzabcdefghij", + UserID: 1, + Name: "App2", + } +} + +// GetAllApplications returns all mock-applications as a list +func GetAllApplications() []*model.Application { + applications := make([]*model.Application, 0) + applications = append(applications, GetApplication1()) + applications = append(applications, GetApplication2()) + + return applications +} diff --git a/tests/mockups/config.go b/tests/mockups/config.go new file mode 100644 index 0000000..d87ea65 --- /dev/null +++ b/tests/mockups/config.go @@ -0,0 +1,43 @@ +package mockups + +import ( + "errors" + "io/ioutil" + "log" + "os" + + "github.com/pushbits/server/internal/configuration" +) + +// ReadConfig copies the given filename to the current folder and parses it as a config file. RemoveFile indicates whether to remove the copied file or not +func ReadConfig(filename string, removeFile bool) (config *configuration.Configuration, err error) { + defer func() { + if r := recover(); r != nil { + log.Println(r) + err = errors.New("paniced while reading config") + } + }() + + if filename == "" { + return nil, errors.New("empty filename") + } + + file, err := ioutil.ReadFile(filename) + + if err != nil { + return nil, err + } + + err = ioutil.WriteFile("config.yml", file, 0644) + if err != nil { + return nil, err + } + + config = configuration.Get() + + if removeFile { + os.Remove("config.yml") + } + + return config, nil +} diff --git a/tests/mockups/database.go b/tests/mockups/database.go new file mode 100644 index 0000000..40348d5 --- /dev/null +++ b/tests/mockups/database.go @@ -0,0 +1,52 @@ +package mockups + +import ( + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/database" + "github.com/pushbits/server/internal/model" +) + +// GetEmptyDatabase returns an empty sqlite database object +func GetEmptyDatabase(confCrypto configuration.CryptoConfig) (*database.Database, error) { + cm := credentials.CreateManager(false, confCrypto) + return database.Create(cm, "sqlite3", "pushbits-test.db") +} + +// AddApplicationsToDb inserts the applications apps into the database db +func AddApplicationsToDb(db *database.Database, apps []*model.Application) error { + for _, app := range apps { + err := db.CreateApplication(app) + if err != nil { + return err + } + } + + return nil +} + +// AddUsersToDb adds the users to the database and sets their username as a password, returns list of added users +func AddUsersToDb(db *database.Database, users []*model.User) ([]*model.User, error) { + addedUsers := make([]*model.User, 0) + + for _, user := range users { + extUser := model.ExternalUser{ + ID: user.ID, + Name: user.Name, + IsAdmin: user.IsAdmin, + MatrixID: user.MatrixID, + } + credentials := model.UserCredentials{ + Password: user.Name, + } + createUser := model.CreateUser{ExternalUser: extUser, UserCredentials: credentials} + + newUser, err := db.CreateUser(createUser) + addedUsers = append(addedUsers, newUser) + if err != nil { + return nil, err + } + } + + return addedUsers, nil +} diff --git a/tests/mockups/dispatcher.go b/tests/mockups/dispatcher.go new file mode 100644 index 0000000..b0accab --- /dev/null +++ b/tests/mockups/dispatcher.go @@ -0,0 +1,23 @@ +package mockups + +import ( + "fmt" + + "github.com/pushbits/server/internal/model" +) + +// MockDispatcher is a dispatcher used for testing - it does not need any storage interface +type MockDispatcher struct { +} + +func (d *MockDispatcher) RegisterApplication(id uint, name, token, user string) (string, error) { + return fmt.Sprintf("%d-%s", id, name), nil +} + +func (d *MockDispatcher) DeregisterApplication(a *model.Application, u *model.User) error { + return nil +} + +func (d *MockDispatcher) UpdateApplication(a *model.Application) error { + return nil +} diff --git a/tests/mockups/user.go b/tests/mockups/user.go new file mode 100644 index 0000000..2e75639 --- /dev/null +++ b/tests/mockups/user.go @@ -0,0 +1,43 @@ +package mockups + +import ( + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/model" +) + +// GetAdminUser returns an admin user +func GetAdminUser(c *configuration.Configuration) *model.User { + credentialsManager := credentials.CreateManager(false, c.Crypto) + hash, _ := credentialsManager.CreatePasswordHash(c.Admin.Password) + + return &model.User{ + ID: 1, + Name: c.Admin.Name, + PasswordHash: hash, + IsAdmin: true, + MatrixID: c.Admin.MatrixID, + } +} + +// GetUser returns an user +func GetUser(c *configuration.Configuration) *model.User { + credentialsManager := credentials.CreateManager(false, c.Crypto) + hash, _ := credentialsManager.CreatePasswordHash(c.Admin.Password) + + return &model.User{ + ID: 2, + Name: c.Admin.Name + "-normalo", + PasswordHash: hash, + IsAdmin: false, + MatrixID: c.Admin.MatrixID, + } +} + +// GetUsers returns a list of users +func GetUsers(c *configuration.Configuration) []*model.User { + var users []*model.User + users = append(users, GetAdminUser(c)) + users = append(users, GetUser(c)) + return users +} diff --git a/tests/request.go b/tests/request.go new file mode 100644 index 0000000..bb786af --- /dev/null +++ b/tests/request.go @@ -0,0 +1,47 @@ +package tests + +import ( + "encoding/json" + "io" + "net/http/httptest" + "strings" + + "github.com/gin-gonic/gin" +) + +// Request holds information for a HTTP request +type Request struct { + Name string + Method string + Endpoint string + Data interface{} + Headers map[string]string + ShouldStatus int +} + +// GetRequest returns a ResponseRecorder and gin context according to the data set in the Request. +// String data is passed as is, all other data types are marshaled before. +func (r *Request) GetRequest() (w *httptest.ResponseRecorder, c *gin.Context, err error) { + var body io.Reader + w = httptest.NewRecorder() + + switch r.Data.(type) { + case string: + body = strings.NewReader(r.Data.(string)) + default: + dataMarshaled, err := json.Marshal(r.Data) + if err != nil { + return nil, nil, err + } + body = strings.NewReader(string(dataMarshaled)) + } + + c, _ = gin.CreateTestContext(w) + c.Request = httptest.NewRequest(r.Method, r.Endpoint, body) + + for name, value := range r.Headers { + c.Request.Header.Set(name, value) + } + + return w, c, nil +}