Merge branch 'pushbits:master' into master

This commit is contained in:
Cubicroot 2021-07-21 18:34:34 +02:00 committed by GitHub
commit 944e7da818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1037 additions and 107 deletions

View file

@ -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

29
.github/workflows/publish.yml vendored Normal file
View file

@ -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

View file

@ -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:

View file

@ -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
```

View file

@ -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

9
go.mod
View file

@ -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
)

49
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}
}

View file

@ -1,5 +0,0 @@
package api
import "errors"
var ErrorMessageNotFound = errors.New("message not found")

View file

@ -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)

View file

@ -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"}
}

View file

@ -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")
}

View file

@ -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

View file

@ -0,0 +1,6 @@
package pberrors
import "errors"
// ErrorMessageNotFound indicates that a message does not exist
var ErrorMessageNotFound = errors.New("message not found")

View file

@ -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
}

43
tests/mockups/config.go Normal file
View file

@ -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
}

52
tests/mockups/database.go Normal file
View file

@ -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
}

View file

@ -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
}

43
tests/mockups/user.go Normal file
View file

@ -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
}

47
tests/request.go Normal file
View file

@ -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
}