mirror of
https://github.com/elyby/chrly.git
synced 2025-03-13 23:34:23 +05:30
Merge branch 'develop'
This commit is contained in:
commit
befa163f0e
@ -1,5 +1,2 @@
|
|||||||
# Игнорим данные, т.к. они не нужны для внутреннего содержимого этого контейнера
|
|
||||||
data
|
data
|
||||||
|
|
||||||
# Vendor так же не нужен
|
|
||||||
vendor
|
vendor
|
||||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,15 +1,5 @@
|
|||||||
# IDEA
|
.idea
|
||||||
/.idea
|
docker-compose.yml
|
||||||
|
docker-compose.override.yml
|
||||||
# Docker Compose file
|
vendor
|
||||||
/docker-compose.yml
|
|
||||||
/docker-compose.override.yml
|
|
||||||
|
|
||||||
# vendor
|
|
||||||
/vendor
|
|
||||||
|
|
||||||
# Cover output
|
|
||||||
.cover
|
.cover
|
||||||
|
|
||||||
# Local config
|
|
||||||
/config.yml
|
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
# Предполагается, что между работой "build docker container" и этапом push
|
|
||||||
# построенные docker images остаются статичными и никуда не пропадают
|
|
||||||
#
|
|
||||||
# В противном случае их нужно после каждого этапа билда пушить в registry
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- build
|
|
||||||
- build_docker_image
|
|
||||||
- push
|
|
||||||
- cleanup
|
|
||||||
|
|
||||||
variables:
|
|
||||||
CONTAINER_IMAGE: registry.ely.by/elyby/skinsystem
|
|
||||||
|
|
||||||
.golang_template: &setup_go_environment
|
|
||||||
image: golang:1.9.0-alpine3.6
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache git
|
|
||||||
- mkdir -p $GOPATH/src/$CI_PROJECT_NAMESPACE
|
|
||||||
- cp -r $(pwd) $GOPATH/src/$CI_PROJECT_PATH
|
|
||||||
- cd $GOPATH/src/$CI_PROJECT_PATH
|
|
||||||
- go get -u github.com/golang/dep/cmd/dep
|
|
||||||
- $GOPATH/bin/dep ensure
|
|
||||||
|
|
||||||
.docker_template: &setup_docker_environment
|
|
||||||
image: docker:latest
|
|
||||||
before_script:
|
|
||||||
- docker login -u gitlab-ci -p $CI_JOB_TOKEN registry.ely.by
|
|
||||||
- export TEMP_IMAGE_NAME="$CONTAINER_IMAGE:$CI_PIPELINE_ID"
|
|
||||||
|
|
||||||
test:
|
|
||||||
<<: *setup_go_environment
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- ./script/coverage
|
|
||||||
|
|
||||||
build executable:
|
|
||||||
<<: *setup_go_environment
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- export VERSION="${CI_COMMIT_TAG:-dev-$CI_COMMIT_REF_NAME-${CI_COMMIT_SHA:0:8}+build-$CI_JOB_ID}"
|
|
||||||
- >
|
|
||||||
env GOOS=linux
|
|
||||||
go build
|
|
||||||
-o $CI_PROJECT_DIR/minecraft-skinsystem
|
|
||||||
-ldflags "-X ${CI_PROJECT_PATH}/bootstrap.version=${VERSION}"
|
|
||||||
main.go
|
|
||||||
artifacts:
|
|
||||||
name: "${CI_JOB_STAGE} executable"
|
|
||||||
paths:
|
|
||||||
- $CI_PROJECT_DIR/minecraft-skinsystem
|
|
||||||
expire_in: 1 day
|
|
||||||
|
|
||||||
build docker image:
|
|
||||||
<<: *setup_docker_environment
|
|
||||||
stage: build_docker_image
|
|
||||||
script:
|
|
||||||
- docker build -t $TEMP_IMAGE_NAME -f docker/Dockerfile .
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
- develop
|
|
||||||
|
|
||||||
push dev:
|
|
||||||
<<: *setup_docker_environment
|
|
||||||
stage: push
|
|
||||||
variables:
|
|
||||||
GIT_STRATEGY: none
|
|
||||||
script:
|
|
||||||
- export IMAGE_NAME="$CONTAINER_IMAGE:dev"
|
|
||||||
- docker tag $TEMP_IMAGE_NAME $IMAGE_NAME
|
|
||||||
- docker push $IMAGE_NAME
|
|
||||||
only:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
push tag:
|
|
||||||
<<: *setup_docker_environment
|
|
||||||
stage: push
|
|
||||||
variables:
|
|
||||||
GIT_STRATEGY: none
|
|
||||||
script:
|
|
||||||
- export IMAGE_NAME="$CONTAINER_IMAGE:$CI_COMMIT_TAG"
|
|
||||||
- export LATEST_IMAGE_NAME="$CONTAINER_IMAGE:latest"
|
|
||||||
- docker tag $TEMP_IMAGE_NAME $IMAGE_NAME
|
|
||||||
- docker tag $TEMP_IMAGE_NAME $LATEST_IMAGE_NAME
|
|
||||||
- docker push $IMAGE_NAME
|
|
||||||
- docker push $LATEST_IMAGE_NAME
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
||||||
cleanup temp image:
|
|
||||||
<<: *setup_docker_environment
|
|
||||||
stage: cleanup
|
|
||||||
when: always
|
|
||||||
script:
|
|
||||||
- docker rmi $TEMP_IMAGE_NAME || true
|
|
34
.travis.yml
Normal file
34
.travis.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
sudo: required
|
||||||
|
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.9
|
||||||
|
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- publish
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get -u github.com/golang/dep/cmd/dep
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- stage: test
|
||||||
|
script:
|
||||||
|
- dep ensure
|
||||||
|
- go test ./...
|
||||||
|
- stage: publish
|
||||||
|
script:
|
||||||
|
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||||
|
- dep ensure
|
||||||
|
- >
|
||||||
|
env GOOS=linux
|
||||||
|
go build
|
||||||
|
-o release/chrly
|
||||||
|
-ldflags "-X github.com/elyby/chrly/bootstrap.version=latest"
|
||||||
|
main.go
|
||||||
|
- docker build -t elyby/chrly .
|
||||||
|
- docker push elyby/chrly
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM alpine:3.7
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
ENV STORAGE_REDIS_HOST=redis
|
||||||
|
ENV STORAGE_FILESYSTEM_HOST=/data
|
||||||
|
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
COPY release/chrly /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["serve"]
|
35
Gopkg.lock
generated
35
Gopkg.lock
generated
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/assembla/cony"
|
name = "github.com/SermoDigital/jose"
|
||||||
packages = ["."]
|
packages = [".","crypto","jws","jwt"]
|
||||||
revision = "dd62697b0adb9adfda8589520cb85f4cbc2361f1"
|
revision = "f6df55f235c24f236d11dbcf665249a59ac2021f"
|
||||||
version = "v0.3.2"
|
version = "1.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/certifi/gocertifi"
|
name = "github.com/certifi/gocertifi"
|
||||||
@ -82,8 +82,8 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/mono83/slf"
|
name = "github.com/mono83/slf"
|
||||||
packages = [".","filters","params","rays","recievers","recievers/ansi","recievers/statsd","wd"]
|
packages = [".","filters","params","rays","recievers","recievers/sentry","recievers/statsd","recievers/writer","wd"]
|
||||||
revision = "8188a95c8d6b74c43953abb38b8bd6fdbc412ff5"
|
revision = "79153e9636db86e1c6b74d74dd04176f257a4f2d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
@ -125,7 +125,7 @@
|
|||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/spf13/cobra"
|
name = "github.com/spf13/cobra"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "3c0b56b677e04926dfa835a1b3f11cd4f62f076e"
|
revision = "0c34d16c3123764e413b9ed982ada58b1c3d53ea"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
@ -145,18 +145,19 @@
|
|||||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/streadway/amqp"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "2cbfe40c9341ad63ba23e53013b3ddc7989d801c"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
packages = ["assert"]
|
packages = ["assert"]
|
||||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||||
version = "v1.1.4"
|
version = "v1.1.4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "issue-18"
|
||||||
|
name = "github.com/thedevsaddam/govalidator"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "59055296916bb3c6ad9cf3b21d5f2cf7059f8e76"
|
||||||
|
source = "https://github.com/erickskrauch/govalidator.git"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
@ -169,12 +170,6 @@
|
|||||||
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
||||||
revision = "bd91bbf73e9a4a801adbfb97133c992678533126"
|
revision = "bd91bbf73e9a4a801adbfb97133c992678533126"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "gopkg.in/h2non/gock.v1"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "84d599244901620fb3eb96473eb9e50619f69b47"
|
|
||||||
version = "v1.0.6"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "v2"
|
branch = "v2"
|
||||||
name = "gopkg.in/yaml.v2"
|
name = "gopkg.in/yaml.v2"
|
||||||
@ -184,6 +179,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "dd545fafc23f9b6429b5b679ad5c213c14c819f1e4ea381823acf338651122e1"
|
inputs-digest = "e6bd87f630333e3e5b03bea33720c3281a9094551bd5ced436062157fe51ab71"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
12
Gopkg.toml
12
Gopkg.toml
@ -1,4 +1,4 @@
|
|||||||
ignored = ["elyby/minecraft-skinsystem"]
|
ignored = ["github.com/elyby/chrly"]
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/gorilla/mux"
|
name = "github.com/gorilla/mux"
|
||||||
@ -12,6 +12,7 @@ ignored = ["elyby/minecraft-skinsystem"]
|
|||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/spf13/cobra"
|
name = "github.com/spf13/cobra"
|
||||||
|
branch = "master"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/spf13/viper"
|
name = "github.com/spf13/viper"
|
||||||
@ -20,8 +21,13 @@ ignored = ["elyby/minecraft-skinsystem"]
|
|||||||
name = "github.com/getsentry/raven-go"
|
name = "github.com/getsentry/raven-go"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/assembla/cony"
|
name = "github.com/SermoDigital/jose"
|
||||||
version = "^0.3.2"
|
version = "~1.1.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/thedevsaddam/govalidator"
|
||||||
|
source = "https://github.com/erickskrauch/govalidator.git"
|
||||||
|
branch = "issue-18"
|
||||||
|
|
||||||
# Testing dependencies
|
# Testing dependencies
|
||||||
|
|
||||||
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2018 Ely.by (http://ely.by)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
288
README.md
288
README.md
@ -1,74 +1,252 @@
|
|||||||
# Ely.by Minecraft Skinsystem
|
# Chrly
|
||||||
|
|
||||||
Реализация API системы скинов для Minecraft v4.
|
Chrly is a lightweight implementation of Minecraft skins system server. It's packaged and distributed as a Docker
|
||||||
|
image and can be downloaded from [Dockerhub](https://hub.docker.com/r/elyby/chrly/). App is written in Go, can
|
||||||
|
withstand heavy loads and is production ready.
|
||||||
|
|
||||||
## Config
|
## Installation
|
||||||
|
|
||||||
Конфигурация может задаваться посредством любого из перечисленных форматов файлов: JSON, TOML, YAML, HCL и
|
You can easily install Chrly using [docker-compose](https://docs.docker.com/compose/). The configuration below (save
|
||||||
Java properties. Кроме того, параметры конфигурации могут перезаписываться доступными при запуске программы
|
it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` environment variable
|
||||||
ENV переменными.
|
that you must set before running `docker-compose up -d`. Other possible variables are described below.
|
||||||
|
|
||||||
> **Заметка**: ENV переменные именуются как KEY.SUBKEY.SUBSUBKEY, т.е. все символы должны быть заглавными,
|
```yml
|
||||||
а точки должны отделять уровень вложенности.
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: elyby/chrly
|
||||||
|
hostname: chrly0
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- ./data/capes:/data/capes
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
CHRLY_SECRET: replace_this_value_in_production
|
||||||
|
|
||||||
Пример файла конфигурации находится в [config.dist.yml](config.dist.yml). Внутри dist-файла есть комментарии,
|
redis:
|
||||||
поясняющие назначение тех или иных параметров. Для работы его следует скопировать в локальный `config.yml`
|
image: redis:4.0-32bit
|
||||||
и отредактировать под свои нужды.
|
restart: always
|
||||||
|
volumes:
|
||||||
## Развёртывание
|
- ./data/redis:/data
|
||||||
|
|
||||||
Деплоить проект можно двумя способами:
|
|
||||||
|
|
||||||
1. Скомпилировав и запустив бинарный файл, а также обеспечив ему доступ ко всем необходмым сервисам.
|
|
||||||
|
|
||||||
2. Используя Docker и docker-compose.
|
|
||||||
|
|
||||||
*Первый случай не буду описывать, т.к. долго, мучительно и никто так делать не будет, я гарантирую это*,
|
|
||||||
поэтому перейдём сразу ко второму.
|
|
||||||
|
|
||||||
Прежде всего необходимо установить [Docker](https://docs.docker.com/engine/installation/) и
|
|
||||||
[docker-compose](https://docs.docker.com/compose/install/).
|
|
||||||
|
|
||||||
Для запуска последней версии проекта достаточно скопировать содержимое файла
|
|
||||||
[docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) в файл `docker-compose.yml` непосредственно
|
|
||||||
на месте установки, после чего ввести в консоль команду:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Web-приложение, amqp worker и все сопутствующие сервисы будут автоматически запущены. Данные из контейнеров
|
Chrly will mount some volumes on the host machine to persist storage for capes and Redis database.
|
||||||
будут синхронизироваться в папку `data`.
|
|
||||||
|
|
||||||
## Разработка
|
### Config
|
||||||
|
|
||||||
Перво-наперво необходимо [установить последнюю версию Go](https://golang.org/doc/install) и сконфигурировать
|
Application's configuration is based on the environment variables. You can adjust config by modifying `environment` key
|
||||||
переменную окружения GOPATH, а также установить инструмент контроля версий [dep](https://github.com/golang/dep).
|
inside your `docker-compose.yml` file. After value will have been changed, container should be stopped and recreated.
|
||||||
|
If environment variables have been changed, Docker will automatically recreate the container, so you only need to `stop`
|
||||||
Затем можно склонировать репозиторий хитрым способом, чтобы удовлетворить все прекрасные особенности Go:
|
and `up` it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Сперва создадим подпапку для приватных Go проектов Ely.by
|
docker-compose stop app
|
||||||
mkdir -p $GOPATH/src/elyby
|
docker-compose up -d app
|
||||||
# Затем непосредственно клинируем репозиторий туда, где его ожидает увидеть Go
|
```
|
||||||
git clone git@gitlab.ely.by:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem
|
|
||||||
# Переходим в папку проекта
|
**Variables to adjust:**
|
||||||
cd $GOPATH/src/elyby/minecraft-skinsystem
|
|
||||||
# Устанавливаем зависимости
|
| ENV | Description | Example |
|
||||||
|
|--------------------|------------------------------------------------------------------------------------|-------------------------------------------|
|
||||||
|
| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` |
|
||||||
|
| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` |
|
||||||
|
| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` |
|
||||||
|
|
||||||
|
If something goes wrong, you can always access logs by executing `docker-compose logs -f app`.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
Each endpoint that accepts `username` as a part of an url takes it case insensitive. `.png` part can be omitted too.
|
||||||
|
|
||||||
|
#### `GET /skins/{username}.png`
|
||||||
|
|
||||||
|
This endpoint responds to requested `username` with a skin texture. If user's skin was set as texture's link, then it'll
|
||||||
|
respond with the `301` redirect to that url. If there is no record for requested username, it'll redirect to the
|
||||||
|
Mojang skins system as: `http://skins.minecraft.net/MinecraftSkins/{username}.png` with the original username's case.
|
||||||
|
|
||||||
|
#### `GET /cloaks/{username}.png`
|
||||||
|
|
||||||
|
It responds to requested `username` with a cape texture. If user's cape file doesn't exists, then it'll redirect to the
|
||||||
|
Mojang skins system as: `http://skins.minecraft.net/MinecraftCloaks/{username}.png` with the original username's case.
|
||||||
|
|
||||||
|
#### `GET /textures/{username}`
|
||||||
|
|
||||||
|
This endpoint forms response payloads as if it was the `textures`' property, but without base64 encoding. For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SKIN": {
|
||||||
|
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||||
|
"hash": "55d2a8848764f5ff04012cdb093458bd",
|
||||||
|
"metadata": {
|
||||||
|
"model": "slim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CAPE": {
|
||||||
|
"url": "http://skinsystem.ely.by/cloaks/username",
|
||||||
|
"hash": "424ff79dce9940af89c28ad80de8aaad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If record for the requested username wasn't found, cape would be omitted and skin would be formed for Mojang skins
|
||||||
|
system. Hash would be formed as the username plus the half-hour-ranged time of request, which is needed to improve
|
||||||
|
caching of Mojang skins inside Minecraft.
|
||||||
|
|
||||||
|
That request is handy in case when your server implements authentication for a game server (e.g. join/hasJoined
|
||||||
|
operation) and you have to respond with hasJoined request with an actual user textures. You have to simply send request
|
||||||
|
to the Chrly server and put the result in your hasJoined response.
|
||||||
|
|
||||||
|
#### `GET /textures/signed/{username}`
|
||||||
|
|
||||||
|
Actually, it's [Ely.by](http://ely.by) feature called [Server Skins System](http://ely.by/server-skins-system), but if
|
||||||
|
you have your own source of the Mojang signatures, then you can pass it with textures and it'll be displayed in this
|
||||||
|
method. Received response should be directly sent to the client without any modification via game server API.
|
||||||
|
|
||||||
|
Response example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "signature value",
|
||||||
|
"value": "base64 encoded value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chrly",
|
||||||
|
"value": "how do you tame a horse in Minecraft?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is no requested `username` or `mojangSignature` field isn't set, `204` status code will be sent.
|
||||||
|
|
||||||
|
#### `GET /skins?name={username}`
|
||||||
|
|
||||||
|
Equivalent of the `GET /skins/{username}.png`, but constructed especially for old Minecraft versions, where username
|
||||||
|
placeholder wasn't used.
|
||||||
|
|
||||||
|
#### `GET /cloaks?name={username}`
|
||||||
|
|
||||||
|
Equivalent of the `GET /cloaks/{username}.png`, but constructed especially for old Minecraft versions, where username
|
||||||
|
placeholder wasn't used.
|
||||||
|
|
||||||
|
### Records manipulating API
|
||||||
|
|
||||||
|
Each request to the internal API should be performed with the Bearer authorization header. Example curl request:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST -i http://chrly.domain.com/api/skins \
|
||||||
|
-H "Authorization: Bearer Ym9zY236Ym9zY28="
|
||||||
|
```
|
||||||
|
|
||||||
|
You can obtain token by executing `docker-compose run --rm app token`.
|
||||||
|
|
||||||
|
#### `POST /api/skins`
|
||||||
|
|
||||||
|
> **Warning**: skin uploading via `skin` field is not implemented for now.
|
||||||
|
|
||||||
|
Endpoint allows you to create or update skin record for a username. To upload skin, you have to send multipart
|
||||||
|
form data. `form-urlencoded` also supported, but, as you may know, it doesn't support files uploading.
|
||||||
|
|
||||||
|
**Request params:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-----------------|--------|--------------------------------------------------------------------------------|
|
||||||
|
| identityId | int | Unique record identifier. |
|
||||||
|
| username | string | Username. Case insensitive. |
|
||||||
|
| uuid | uuid | UUID of the user. |
|
||||||
|
| skinId | int | Skin identifier. |
|
||||||
|
| hash | string | Skin's hash. Algorithm can be any. For example `md5`. |
|
||||||
|
| is1_8 | bool | Does the skin have the new format (64x64). |
|
||||||
|
| isSlim | bool | Does skin have slim arms (Alex model). |
|
||||||
|
| mojangTextures | string | Mojang textures field. It must be a base64 encoded json string. Not required. |
|
||||||
|
| mojangSignature | string | Signature for Mojang textures, which is required when `mojangTextures` passed. |
|
||||||
|
| url | string | Actual url of the skin. You have to pass this parameter or `skin`. |
|
||||||
|
| skin | file | Skin file. You have to pass this parameter or `url`. |
|
||||||
|
|
||||||
|
If successful you'll receive `201` status code. In the case of failure there will be `400` status code and errors list
|
||||||
|
as json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"identityId": [
|
||||||
|
"The identityId field must be numeric"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DELETE /api/skins/id:{identityId}`
|
||||||
|
|
||||||
|
Performs record removal by identity id. Request body is not required. On success you will receive `204` status code.
|
||||||
|
On failure it'll be `404` with the json body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Cannot find record for requested user id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DELETE /api/skins/{username}`
|
||||||
|
|
||||||
|
Same endpoint as above but it removes record by identity's username. Have the same behavior, but in case of failure
|
||||||
|
response will be:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Cannot find record for requested username"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH`
|
||||||
|
environment variable.
|
||||||
|
|
||||||
|
This project uses [`dep`](https://github.com/golang/dep) for dependencies management, so it
|
||||||
|
[should be installed](https://github.com/golang/dep#installation) too.
|
||||||
|
|
||||||
|
Then you must fork this repository. Now follow these steps:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Get the source code
|
||||||
|
go get github.com/elyby/chrly
|
||||||
|
# Switch to the project folder
|
||||||
|
cd $GOPATH/src/github.com/elyby/chrly
|
||||||
|
# Install dependencies (it can take a while)
|
||||||
dep ensure
|
dep ensure
|
||||||
|
# Add your fork link as a remote
|
||||||
|
git remote add fork git@github.com:your-username/chrly.git
|
||||||
|
# Create a new branch for your task
|
||||||
|
git checkout -b iss-123
|
||||||
```
|
```
|
||||||
|
|
||||||
Чтобы запустить проект достаточно написать `go run main.go`, но без файла конфигурации и Redis
|
You only need to execute `go run main.go` to run the project, but without Redis database and a secret key it won't work
|
||||||
программа долго не проработает. Поэтому сперва копируем `config.dist.yml` в `config.yml` и, при необходимости,
|
for very long. You have to export `CHRLY_SECRET` environment variable globally or pass it via `env`:
|
||||||
затачиваем его под себя.
|
|
||||||
|
|
||||||
Redis можно установить в систему самостоятельно, но гораздо удобнее воспользоваться готовыми сервисами,
|
|
||||||
описанными в [docker/docker-compose.dev.yml](docker/docker-compose.dev.yml). Для этого просто копируем
|
|
||||||
`docker-compose.dev.yml` и поднимаем сервисы:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp docker/docker-compose.dev.yml docker-compose.yml
|
env CHRLY_SECRET=some_local_secret go run main.go serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis can be installed manually, but if you have [Docker installed](https://docs.docker.com/install/), you can run
|
||||||
|
predefined docker-compose service. Simply execute the next commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp docker-compose.dev.yml docker-compose.yml
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
После этого `go run main.go serve` должен запустить web-сервер без дополнительной модификации файла конфигурации.
|
If your Redis instance isn't located at the `localhost`, you can change host by editing environment variable
|
||||||
|
`STORAGE_REDIS_HOST`.
|
||||||
|
|
||||||
|
After all of that `go run main.go serve` should successfully start the application.
|
||||||
|
To run tests execute `go test ./...`. If your Go version is older than 1.9, then run a `/script/test`.
|
||||||
|
@ -1,166 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Addr string
|
|
||||||
Id string
|
|
||||||
Secret string
|
|
||||||
Scopes []string
|
|
||||||
|
|
||||||
Client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
config *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *Config) GetToken() (*Token, error) {
|
|
||||||
form := url.Values{}
|
|
||||||
form.Add("client_id", config.Id)
|
|
||||||
form.Add("client_secret", config.Secret)
|
|
||||||
form.Add("grant_type", "client_credentials")
|
|
||||||
form.Add("scope", strings.Join(config.Scopes, ","))
|
|
||||||
|
|
||||||
response, err := config.getHttpClient().Post(config.getTokenUrl(), "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var result *Token
|
|
||||||
responseError := handleResponse(response)
|
|
||||||
if responseError != nil {
|
|
||||||
return nil, responseError
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
unmarshalError := json.Unmarshal(body, &result)
|
|
||||||
if unmarshalError != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result.config = config
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *Config) getTokenUrl() string {
|
|
||||||
return concatenateHostAndPath(config.Addr, "/api/oauth2/v1/token")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *Config) getHttpClient() *http.Client {
|
|
||||||
if config.Client == nil {
|
|
||||||
config.Client = &http.Client{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountInfoResponse struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (token *Token) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) {
|
|
||||||
request := token.newRequest("GET", token.accountInfoUrl(), nil)
|
|
||||||
|
|
||||||
query := request.URL.Query()
|
|
||||||
query.Add(attribute, value)
|
|
||||||
request.URL.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
response, err := token.config.Client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var info *AccountInfoResponse
|
|
||||||
|
|
||||||
responseError := handleResponse(response)
|
|
||||||
if responseError != nil {
|
|
||||||
return nil, responseError
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
json.Unmarshal(body, &info)
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (token *Token) accountInfoUrl() string {
|
|
||||||
return concatenateHostAndPath(token.config.Addr, "/api/internal/accounts/info")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (token *Token) newRequest(method string, urlStr string, body io.Reader) *http.Request {
|
|
||||||
request, err := http.NewRequest(method, urlStr, body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Add("Authorization", "Bearer " + token.AccessToken)
|
|
||||||
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
func concatenateHostAndPath(host string, pathToJoin string) string {
|
|
||||||
u, _ := url.Parse(host)
|
|
||||||
u.Path = path.Join(u.Path, pathToJoin)
|
|
||||||
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnauthorizedResponse struct {}
|
|
||||||
|
|
||||||
func (err UnauthorizedResponse) Error() string {
|
|
||||||
return "Unauthorized response"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForbiddenResponse struct {}
|
|
||||||
|
|
||||||
func (err ForbiddenResponse) Error() string {
|
|
||||||
return "Forbidden response"
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotFoundResponse struct {}
|
|
||||||
|
|
||||||
func (err NotFoundResponse) Error() string {
|
|
||||||
return "Not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotSuccessResponse struct {
|
|
||||||
StatusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err NotSuccessResponse) Error() string {
|
|
||||||
return fmt.Sprintf("Response code is \"%d\"", err.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResponse(response *http.Response) error {
|
|
||||||
switch status := response.StatusCode; status {
|
|
||||||
case 200:
|
|
||||||
return nil
|
|
||||||
case 401:
|
|
||||||
return &UnauthorizedResponse{}
|
|
||||||
case 403:
|
|
||||||
return &ForbiddenResponse{}
|
|
||||||
case 404:
|
|
||||||
return &NotFoundResponse{}
|
|
||||||
default:
|
|
||||||
return &NotSuccessResponse{status}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
testify "github.com/stretchr/testify/assert"
|
|
||||||
"gopkg.in/h2non/gock.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig_GetToken(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
config := &Config{
|
|
||||||
Addr: "https://account.ely.by",
|
|
||||||
Id: "mock-id",
|
|
||||||
Secret: "mock-secret",
|
|
||||||
Scopes: []string{"scope1", "scope2"},
|
|
||||||
Client: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := config.GetToken()
|
|
||||||
if assert.NoError(err) {
|
|
||||||
assert.Equal("mocked-token", result.AccessToken)
|
|
||||||
assert.Equal("Bearer", result.TokenType)
|
|
||||||
assert.Equal(86400, result.ExpiresIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_AccountInfo(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
// To test valid behavior
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mock-token").
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"id": 1,
|
|
||||||
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
|
||||||
"username": "dummy",
|
|
||||||
"email": "dummy@ely.by",
|
|
||||||
})
|
|
||||||
|
|
||||||
// To test behavior on invalid or expired token
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mock-token").
|
|
||||||
Reply(401).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"name": "Unauthorized",
|
|
||||||
"message": "Incorrect token",
|
|
||||||
"code": 0,
|
|
||||||
"status": 401,
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
token := &Token{
|
|
||||||
AccessToken: "mock-token",
|
|
||||||
config: &Config{
|
|
||||||
Addr: "https://account.ely.by",
|
|
||||||
Client: client,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := token.AccountInfo("id", "1")
|
|
||||||
if assert.NoError(err) {
|
|
||||||
assert.Equal(1, result.Id)
|
|
||||||
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
|
||||||
assert.Equal("dummy", result.Username)
|
|
||||||
assert.Equal("dummy@ely.by", result.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
result2, err2 := token.AccountInfo("id", "1")
|
|
||||||
assert.Nil(result2)
|
|
||||||
assert.Error(err2)
|
|
||||||
assert.IsType(&UnauthorizedResponse{}, err2)
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
type AutoRefresh struct {
|
|
||||||
token *Token
|
|
||||||
config *Config
|
|
||||||
repeatsCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
const repeatsLimit = 3
|
|
||||||
|
|
||||||
func (config *Config) GetTokenWithAutoRefresh() *AutoRefresh {
|
|
||||||
return &AutoRefresh{
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (refresher *AutoRefresh) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) {
|
|
||||||
defer refresher.resetRepeatsCount()
|
|
||||||
|
|
||||||
apiToken, err := refresher.getToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := apiToken.AccountInfo(attribute, value)
|
|
||||||
if err != nil {
|
|
||||||
_, isTokenExpire := err.(*UnauthorizedResponse)
|
|
||||||
if !isTokenExpire || refresher.repeatsCount >= repeatsLimit - 1 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
refresher.repeatsCount++
|
|
||||||
refresher.token = nil
|
|
||||||
|
|
||||||
return refresher.AccountInfo(attribute, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (refresher *AutoRefresh) getToken() (*Token, error) {
|
|
||||||
if refresher.token == nil {
|
|
||||||
newToken, err := refresher.config.GetToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
refresher.token = newToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return refresher.token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (refresher *AutoRefresh) resetRepeatsCount() {
|
|
||||||
refresher.repeatsCount = 0
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
testify "github.com/stretchr/testify/assert"
|
|
||||||
"gopkg.in/h2non/gock.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var config = &Config{
|
|
||||||
Addr: "https://account.ely.by",
|
|
||||||
Id: "mock-id",
|
|
||||||
Secret: "mock-secret",
|
|
||||||
Scopes: []string{"scope1", "scope2"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_GetTokenWithAutoRefresh(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
testConfig := &Config{}
|
|
||||||
*testConfig = *config
|
|
||||||
|
|
||||||
result := testConfig.GetTokenWithAutoRefresh()
|
|
||||||
assert.Equal(testConfig, result.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoRefresh_AccountInfo(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
Times(2).
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token").
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"id": 1,
|
|
||||||
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
|
||||||
"username": "dummy",
|
|
||||||
"email": "dummy@ely.by",
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
testConfig := &Config{}
|
|
||||||
*testConfig = *config
|
|
||||||
testConfig.Client = client
|
|
||||||
|
|
||||||
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
|
||||||
result, err := autoRefresher.AccountInfo("id", "1")
|
|
||||||
if assert.NoError(err) {
|
|
||||||
assert.Equal(1, result.Id)
|
|
||||||
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
|
||||||
assert.Equal("dummy", result.Username)
|
|
||||||
assert.Equal("dummy@ely.by", result.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
result2, err2 := autoRefresher.AccountInfo("id", "1")
|
|
||||||
if assert.NoError(err2) {
|
|
||||||
assert.Equal(result, result2, "Results should still be same without token refreshing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoRefresh_AccountInfo2(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token-1",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token-1").
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"id": 1,
|
|
||||||
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
|
||||||
"username": "dummy",
|
|
||||||
"email": "dummy@ely.by",
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token-1").
|
|
||||||
Reply(401).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"name": "Unauthorized",
|
|
||||||
"message": "Incorrect token",
|
|
||||||
"code": 0,
|
|
||||||
"status": 401,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token-2",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token-2").
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"id": 1,
|
|
||||||
"uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
|
||||||
"username": "dummy",
|
|
||||||
"email": "dummy@ely.by",
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
testConfig := &Config{}
|
|
||||||
*testConfig = *config
|
|
||||||
testConfig.Client = client
|
|
||||||
|
|
||||||
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
|
||||||
result, err := autoRefresher.AccountInfo("id", "1")
|
|
||||||
if assert.NoError(err) {
|
|
||||||
assert.Equal(1, result.Id)
|
|
||||||
assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid)
|
|
||||||
assert.Equal("dummy", result.Username)
|
|
||||||
assert.Equal("dummy@ely.by", result.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
result2, err2 := autoRefresher.AccountInfo("id", "1")
|
|
||||||
if assert.NoError(err2) {
|
|
||||||
assert.Equal(result, result2, "Results should still be same with refreshed token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoRefresh_AccountInfo3(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token-1",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token-1").
|
|
||||||
Reply(404).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"name": "Not Found",
|
|
||||||
"message": "Page not found.",
|
|
||||||
"code": 0,
|
|
||||||
"status": 404,
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
testConfig := &Config{}
|
|
||||||
*testConfig = *config
|
|
||||||
testConfig.Client = client
|
|
||||||
|
|
||||||
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
|
||||||
result, err := autoRefresher.AccountInfo("id", "1")
|
|
||||||
assert.Nil(result)
|
|
||||||
assert.Error(err)
|
|
||||||
assert.IsType(&NotFoundResponse{}, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoRefresh_AccountInfo4(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
defer gock.Off()
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Post("/api/oauth2/v1/token").
|
|
||||||
Times(3).
|
|
||||||
Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")).
|
|
||||||
Reply(200).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"access_token": "mocked-token-1",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 86400,
|
|
||||||
})
|
|
||||||
|
|
||||||
gock.New("https://account.ely.by").
|
|
||||||
Get("/api/internal/accounts/info").
|
|
||||||
Times(3).
|
|
||||||
MatchParam("id", "1").
|
|
||||||
MatchHeader("Authorization", "Bearer mocked-token-1").
|
|
||||||
Reply(401).
|
|
||||||
JSON(map[string]interface{}{
|
|
||||||
"name": "Unauthorized",
|
|
||||||
"message": "Incorrect token",
|
|
||||||
"code": 0,
|
|
||||||
"status": 401,
|
|
||||||
})
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
gock.InterceptClient(client)
|
|
||||||
|
|
||||||
testConfig := &Config{}
|
|
||||||
*testConfig = *config
|
|
||||||
testConfig.Client = client
|
|
||||||
|
|
||||||
autoRefresher := testConfig.GetTokenWithAutoRefresh()
|
|
||||||
result, err := autoRefresher.AccountInfo("id", "1")
|
|
||||||
assert.Nil(result)
|
|
||||||
assert.Error(err)
|
|
||||||
if !assert.IsType(&UnauthorizedResponse{}, err) {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
82
auth/jwt.go
Normal file
82
auth/jwt.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SermoDigital/jose/crypto"
|
||||||
|
"github.com/SermoDigital/jose/jws"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hashAlg = crypto.SigningMethodHS256
|
||||||
|
|
||||||
|
const scopesClaim = "scopes"
|
||||||
|
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
var (
|
||||||
|
SkinScope = Scope("skin")
|
||||||
|
)
|
||||||
|
|
||||||
|
type JwtAuth struct {
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) {
|
||||||
|
if len(t.Key) == 0 {
|
||||||
|
return nil, errors.New("signing key not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jws.Claims{}
|
||||||
|
claims.Set(scopesClaim, scopes)
|
||||||
|
claims.SetIssuedAt(time.Now())
|
||||||
|
encoder := jws.NewJWT(claims, hashAlg)
|
||||||
|
token, err := encoder.Serialize(t.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JwtAuth) Check(req *http.Request) error {
|
||||||
|
if len(t.Key) == 0 {
|
||||||
|
return &Unauthorized{"Signing key not set"}
|
||||||
|
}
|
||||||
|
|
||||||
|
bearerToken := req.Header.Get("Authorization")
|
||||||
|
if bearerToken == "" {
|
||||||
|
return &Unauthorized{"Authentication header not presented"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(bearerToken[0:7], "BEARER ") {
|
||||||
|
return &Unauthorized{"Cannot recognize JWT token in passed value"}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := bearerToken[7:]
|
||||||
|
token, err := jws.ParseJWT([]byte(tokenStr))
|
||||||
|
if err != nil {
|
||||||
|
return &Unauthorized{"Cannot parse passed JWT token"}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = token.Validate(t.Key, hashAlg)
|
||||||
|
if err != nil {
|
||||||
|
return &Unauthorized{"JWT token have invalid signature. It may be corrupted or expired."}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unauthorized struct {
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Unauthorized) Error() string {
|
||||||
|
if e.Reason != "" {
|
||||||
|
return e.Reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unauthorized"
|
||||||
|
}
|
97
auth/jwt_test.go
Normal file
97
auth/jwt_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8"
|
||||||
|
|
||||||
|
func TestJwtAuth_NewToken_Success(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
jwt := &JwtAuth{[]byte("secret")}
|
||||||
|
token, err := jwt.NewToken(SkinScope)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.NotNil(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_NewToken_KeyNotAvailable(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
jwt := &JwtAuth{}
|
||||||
|
token, err := jwt.NewToken(SkinScope)
|
||||||
|
assert.Error(err, "signing key not available")
|
||||||
|
assert.Nil(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_EmptyRequest(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
jwt := &JwtAuth{[]byte("secret")}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.IsType(&Unauthorized{}, err)
|
||||||
|
assert.EqualError(err, "Authentication header not presented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_NonBearer(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
req.Header.Add("Authorization", "this is not jwt")
|
||||||
|
jwt := &JwtAuth{[]byte("secret")}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.IsType(&Unauthorized{}, err)
|
||||||
|
assert.EqualError(err, "Cannot recognize JWT token in passed value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt")
|
||||||
|
jwt := &JwtAuth{[]byte("secret")}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.IsType(&Unauthorized{}, err)
|
||||||
|
assert.EqualError(err, "Cannot parse passed JWT token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||||
|
jwt := &JwtAuth{}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.Error(err, "Signing key not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_SecretInvalid(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||||
|
jwt := &JwtAuth{[]byte("this is another secret")}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.IsType(&Unauthorized{}, err)
|
||||||
|
assert.EqualError(err, "JWT token have invalid signature. It may be corrupted or expired.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtAuth_Check_Valid(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||||
|
jwt := &JwtAuth{[]byte("secret")}
|
||||||
|
|
||||||
|
err := jwt.Check(req)
|
||||||
|
assert.Nil(err)
|
||||||
|
}
|
@ -1,18 +1,14 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/assembla/cony"
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/raven-go"
|
||||||
"github.com/mono83/slf/rays"
|
"github.com/mono83/slf/rays"
|
||||||
|
"github.com/mono83/slf/recievers/sentry"
|
||||||
"github.com/mono83/slf/recievers/statsd"
|
"github.com/mono83/slf/recievers/statsd"
|
||||||
"github.com/mono83/slf/recievers/writer"
|
"github.com/mono83/slf/recievers/writer"
|
||||||
"github.com/mono83/slf/wd"
|
"github.com/mono83/slf/wd"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/logger/receivers/sentry"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = ""
|
var version = ""
|
||||||
@ -66,26 +62,3 @@ func CreateLogger(statsdAddr string, sentryAddr string) (wd.Watchdog, error) {
|
|||||||
|
|
||||||
return wd.New("", "").WithParams(rays.Host), nil
|
return wd.New("", "").WithParams(rays.Host), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RabbitMQConfig struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
Vhost string
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateRabbitMQClient(config *RabbitMQConfig) *cony.Client {
|
|
||||||
addr := fmt.Sprintf(
|
|
||||||
"amqp://%s:%s@%s:%d/%s",
|
|
||||||
config.Username,
|
|
||||||
config.Password,
|
|
||||||
config.Host,
|
|
||||||
config.Port,
|
|
||||||
url.PathEscape(config.Vhost),
|
|
||||||
)
|
|
||||||
|
|
||||||
client := cony.NewClient(cony.URL(addr), cony.Backoff(cony.DefaultBackoff))
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/api/accounts"
|
|
||||||
"elyby/minecraft-skinsystem/bootstrap"
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
|
||||||
"elyby/minecraft-skinsystem/worker"
|
|
||||||
)
|
|
||||||
|
|
||||||
var amqpWorkerCmd = &cobra.Command{
|
|
||||||
Use: "amqp-worker",
|
|
||||||
Short: "Launches a worker which listens to events and processes them",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(fmt.Printf("Cannot initialize logger: %v", err))
|
|
||||||
}
|
|
||||||
logger.Info("Logger successfully initialized")
|
|
||||||
|
|
||||||
storageFactory := db.StorageFactory{Config: viper.GetViper()}
|
|
||||||
|
|
||||||
logger.Info("Initializing skins repository")
|
|
||||||
skinsRepo, err := storageFactory.CreateFactory("redis").CreateSkinsRepository()
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency(fmt.Sprintf("Error on creating skins repo: %+v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("Skins repository successfully initialized")
|
|
||||||
|
|
||||||
logger.Info("Creating AMQP client")
|
|
||||||
amqpClient := bootstrap.CreateRabbitMQClient(&bootstrap.RabbitMQConfig{
|
|
||||||
Host: viper.GetString("amqp.host"),
|
|
||||||
Port: viper.GetInt("amqp.port"),
|
|
||||||
Username: viper.GetString("amqp.username"),
|
|
||||||
Password: viper.GetString("amqp.password"),
|
|
||||||
Vhost: viper.GetString("amqp.vhost"),
|
|
||||||
})
|
|
||||||
|
|
||||||
accountsApi := (&accounts.Config{
|
|
||||||
Addr: viper.GetString("api.accounts.host"),
|
|
||||||
Id: viper.GetString("api.accounts.id"),
|
|
||||||
Secret: viper.GetString("api.accounts.secret"),
|
|
||||||
Scopes: viper.GetStringSlice("api.accounts.scopes"),
|
|
||||||
}).GetTokenWithAutoRefresh()
|
|
||||||
|
|
||||||
services := &worker.Services{
|
|
||||||
Logger: logger,
|
|
||||||
AmqpClient: amqpClient,
|
|
||||||
SkinsRepo: skinsRepo,
|
|
||||||
AccountsAPI: accountsApi,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := services.Run(); err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("Cannot initialize worker: %+v", err))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(amqpWorkerCmd)
|
|
||||||
}
|
|
25
cmd/root.go
25
cmd/root.go
@ -3,16 +3,18 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/bootstrap"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cfgFile string
|
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
Use: "",
|
Use: "chrly",
|
||||||
Short: "Nothing here",
|
Short: "Implementation of Minecraft skins system server",
|
||||||
|
Version: bootstrap.GetVersion(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
@ -26,21 +28,10 @@ func Execute() {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if cfgFile != "" {
|
|
||||||
viper.SetConfigFile(cfgFile)
|
|
||||||
} else {
|
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.AddConfigPath("/etc/minecraft-skinsystem")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
replacer := strings.NewReplacer(".", "_")
|
||||||
if err := viper.ReadInConfig(); err == nil {
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
24
cmd/serve.go
24
cmd/serve.go
@ -4,17 +4,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/auth"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/bootstrap"
|
"github.com/elyby/chrly/bootstrap"
|
||||||
"elyby/minecraft-skinsystem/db"
|
"github.com/elyby/chrly/db"
|
||||||
"elyby/minecraft-skinsystem/http"
|
"github.com/elyby/chrly/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serveCmd = &cobra.Command{
|
var serveCmd = &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Runs the system server skins",
|
Short: "Starts http handler for the skins system",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,9 +44,10 @@ var serveCmd = &cobra.Command{
|
|||||||
|
|
||||||
cfg := &http.Config{
|
cfg := &http.Config{
|
||||||
ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")),
|
ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")),
|
||||||
SkinsRepo: skinsRepo,
|
SkinsRepo: skinsRepo,
|
||||||
CapesRepo: capesRepo,
|
CapesRepo: capesRepo,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
Auth: &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.Run(); err != nil {
|
if err := cfg.Run(); err != nil {
|
||||||
@ -55,4 +58,11 @@ var serveCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(serveCmd)
|
RootCmd.AddCommand(serveCmd)
|
||||||
|
viper.SetDefault("server.host", "")
|
||||||
|
viper.SetDefault("server.port", 80)
|
||||||
|
viper.SetDefault("storage.redis.host", "localhost")
|
||||||
|
viper.SetDefault("storage.redis.port", 6379)
|
||||||
|
viper.SetDefault("storage.redis.poll", 10)
|
||||||
|
viper.SetDefault("storage.filesystem.basePath", "data")
|
||||||
|
viper.SetDefault("storage.filesystem.capesDirName", "capes")
|
||||||
}
|
}
|
||||||
|
29
cmd/token.go
Normal file
29
cmd/token.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/auth"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenCmd = &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Creates a new token, which allows to interact with Chrly API",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jwtAuth := &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))}
|
||||||
|
token, err := jwtAuth.NewToken(auth.SkinScope)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to create new token. The error is %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", token)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(tokenCmd)
|
||||||
|
}
|
@ -4,13 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"elyby/minecraft-skinsystem/bootstrap"
|
"github.com/elyby/chrly/bootstrap"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Show the Minecraft Skinsystem version information",
|
Short: "Show the Chrly version information",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Printf("Version: %s\n", bootstrap.GetVersion())
|
fmt.Printf("Version: %s\n", bootstrap.GetVersion())
|
||||||
fmt.Printf("Go version: %s\n", runtime.Version())
|
fmt.Printf("Go version: %s\n", runtime.Version())
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
# Main server configuration. Actually you don't want to change it,
|
|
||||||
# but you able to change host or port, that will be used by serve command
|
|
||||||
server:
|
|
||||||
host: localhost
|
|
||||||
port: 80
|
|
||||||
|
|
||||||
# Worker listen to AMQP events, so it should know how to connect to any
|
|
||||||
# AMQP provider (actually RabbitMQ). You should not escape any vhost
|
|
||||||
# characters, 'cause it will be done by application automatically
|
|
||||||
amqp:
|
|
||||||
host: localhost
|
|
||||||
port: 5672
|
|
||||||
username: amqp-user
|
|
||||||
password: amqp-password
|
|
||||||
vhost: /
|
|
||||||
|
|
||||||
# Both of web or worker depends on storage.
|
|
||||||
storage:
|
|
||||||
# For now app require Redis and don't support any other backends to store
|
|
||||||
# skins, but in the future we can have more backends. Poll size tune amount
|
|
||||||
# of connections to the redis. It's not recommended to set it less then 2
|
|
||||||
# because it will lead to panic on high load.
|
|
||||||
redis:
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
poolSize: 10
|
|
||||||
|
|
||||||
# Filesystem storage used to store capes. basePath specify absolute or relative
|
|
||||||
# path to storage and capesDirName specify which folder in this base path will
|
|
||||||
# be used to search capes.
|
|
||||||
filesystem:
|
|
||||||
basePath: data
|
|
||||||
capesDirName: capes
|
|
||||||
|
|
||||||
# Accounts Ely.by internal API will be used in cases, when by some reasons
|
|
||||||
# information about user will be unavailable in the app storage.
|
|
||||||
api:
|
|
||||||
accounts:
|
|
||||||
host: https://account.ely.by
|
|
||||||
id: app-id
|
|
||||||
secret: secret
|
|
||||||
scopes:
|
|
||||||
- internal_account_info
|
|
||||||
|
|
||||||
# StatsD can be used to collect metrics
|
|
||||||
# statsd:
|
|
||||||
# addr: localhost:3746
|
|
||||||
|
|
||||||
# Sentry can be used to collect app errors
|
|
||||||
# sentry:
|
|
||||||
# dsn: "https://public:private@your.sentry.io/1"
|
|
@ -3,7 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/interfaces"
|
"github.com/elyby/chrly/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StorageFactory struct {
|
type StorageFactory struct {
|
||||||
|
@ -5,8 +5,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/interfaces"
|
"github.com/elyby/chrly/interfaces"
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FilesystemFactory struct {
|
type FilesystemFactory struct {
|
||||||
|
117
db/redis.go
117
db/redis.go
@ -14,17 +14,19 @@ import (
|
|||||||
"github.com/mediocregopher/radix.v2/redis"
|
"github.com/mediocregopher/radix.v2/redis"
|
||||||
"github.com/mediocregopher/radix.v2/util"
|
"github.com/mediocregopher/radix.v2/util"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/interfaces"
|
"github.com/elyby/chrly/interfaces"
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisFactory struct {
|
type RedisFactory struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
PoolSize int
|
PoolSize int
|
||||||
connection util.Cmder
|
connection *pool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: maybe we should manually return connection to the pool?
|
||||||
|
|
||||||
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
||||||
connection, err := f.getConnection()
|
connection, err := f.getConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -38,7 +40,7 @@ func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error
|
|||||||
panic("capes repository not supported for this storage type")
|
panic("capes repository not supported for this storage type")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f RedisFactory) getConnection() (util.Cmder, error) {
|
func (f RedisFactory) getConnection() (*pool.Pool, error) {
|
||||||
if f.connection == nil {
|
if f.connection == nil {
|
||||||
if f.Host == "" {
|
if f.Host == "" {
|
||||||
return nil, &ParamRequired{"host"}
|
return nil, &ParamRequired{"host"}
|
||||||
@ -49,7 +51,7 @@ func (f RedisFactory) getConnection() (util.Cmder, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", f.Host, f.Port)
|
addr := fmt.Sprintf("%s:%d", f.Host, f.Port)
|
||||||
conn, err := createConnection(addr, f.PoolSize)
|
conn, err := pool.New("tcp", addr, f.PoolSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -66,7 +68,7 @@ func (f RedisFactory) getConnection() (util.Cmder, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Redis not pinged. Try to reconnect")
|
log.Println("Redis not pinged. Try to reconnect")
|
||||||
conn, err := createConnection(addr, f.PoolSize)
|
conn, err := pool.New("tcp", addr, f.PoolSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot reconnect to redis: %v\n", err)
|
log.Printf("Cannot reconnect to redis: %v\n", err)
|
||||||
log.Printf("Waiting %d seconds to retry\n", period)
|
log.Printf("Waiting %d seconds to retry\n", period)
|
||||||
@ -82,27 +84,44 @@ func (f RedisFactory) getConnection() (util.Cmder, error) {
|
|||||||
return f.connection, nil
|
return f.connection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createConnection(addr string, poolSize int) (util.Cmder, error) {
|
|
||||||
if poolSize > 1 {
|
|
||||||
return pool.New("tcp", addr, poolSize)
|
|
||||||
} else {
|
|
||||||
return redis.Dial("tcp", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type redisDb struct {
|
type redisDb struct {
|
||||||
conn util.Cmder
|
conn *pool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountIdToUsernameKey string = "hash:username-to-account-id"
|
const accountIdToUsernameKey = "hash:username-to-account-id"
|
||||||
|
|
||||||
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
|
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
|
||||||
|
return findByUsername(username, db.getConn())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
|
||||||
|
return findByUserId(id, db.getConn())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) Save(skin *model.Skin) error {
|
||||||
|
return save(skin, db.getConn())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) RemoveByUserId(id int) error {
|
||||||
|
return removeByUserId(id, db.getConn())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) RemoveByUsername(username string) error {
|
||||||
|
return removeByUsername(username, db.getConn())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *redisDb) getConn() util.Cmder {
|
||||||
|
conn, _ := db.conn.Get()
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, &SkinNotFoundError{username}
|
return nil, &SkinNotFoundError{username}
|
||||||
}
|
}
|
||||||
|
|
||||||
redisKey := buildKey(username)
|
redisKey := buildUsernameKey(username)
|
||||||
response := db.conn.Cmd("GET", redisKey)
|
response := conn.Cmd("GET", redisKey)
|
||||||
if response.IsType(redis.Nil) {
|
if response.IsType(redis.Nil) {
|
||||||
return nil, &SkinNotFoundError{username}
|
return nil, &SkinNotFoundError{username}
|
||||||
}
|
}
|
||||||
@ -128,37 +147,72 @@ func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
|
|||||||
return skin, nil
|
return skin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
|
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
||||||
response := db.conn.Cmd("HGET", accountIdToUsernameKey, id)
|
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
|
||||||
if response.IsType(redis.Nil) {
|
if response.IsType(redis.Nil) {
|
||||||
return nil, &SkinNotFoundError{"unknown"}
|
return nil, &SkinNotFoundError{"unknown"}
|
||||||
}
|
}
|
||||||
|
|
||||||
username, _ := response.Str()
|
username, _ := response.Str()
|
||||||
|
|
||||||
return db.FindByUsername(username)
|
return findByUsername(username, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *redisDb) Save(skin *model.Skin) error {
|
func removeByUserId(id int, conn util.Cmder) error {
|
||||||
conn := db.conn
|
record, err := findByUserId(id, conn)
|
||||||
if poolConn, isPool := conn.(*pool.Pool); isPool {
|
if err != nil {
|
||||||
conn, _ = poolConn.Get()
|
if _, ok := err.(*SkinNotFoundError); !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Cmd("MULTI")
|
conn.Cmd("MULTI")
|
||||||
|
|
||||||
// Если пользователь сменил ник, то мы должны удать его ключ
|
conn.Cmd("HDEL", accountIdToUsernameKey, id)
|
||||||
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
|
if record != nil {
|
||||||
conn.Cmd("DEL", buildKey(skin.OldUsername))
|
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице
|
conn.Cmd("EXEC")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeByUsername(username string, conn util.Cmder) error {
|
||||||
|
record, err := findByUsername(username, conn)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*SkinNotFoundError); !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Cmd("MULTI")
|
||||||
|
|
||||||
|
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||||
|
if record != nil {
|
||||||
|
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Cmd("EXEC")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(skin *model.Skin, conn util.Cmder) error {
|
||||||
|
conn.Cmd("MULTI")
|
||||||
|
|
||||||
|
// If user has changed username, then we must delete his old username record
|
||||||
|
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
|
||||||
|
conn.Cmd("DEL", buildUsernameKey(skin.OldUsername))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a new record or if the user has changed username, we set the value in the hash table
|
||||||
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
|
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
|
||||||
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
|
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
str, _ := json.Marshal(skin)
|
str, _ := json.Marshal(skin)
|
||||||
conn.Cmd("SET", buildKey(skin.Username), zlibEncode(str))
|
conn.Cmd("SET", buildUsernameKey(skin.Username), zlibEncode(str))
|
||||||
|
|
||||||
conn.Cmd("EXEC")
|
conn.Cmd("EXEC")
|
||||||
|
|
||||||
@ -167,11 +221,10 @@ func (db *redisDb) Save(skin *model.Skin) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKey(username string) string {
|
func buildUsernameKey(username string) string {
|
||||||
return "username:" + strings.ToLower(username)
|
return "username:" + strings.ToLower(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
//noinspection GoUnusedFunction
|
|
||||||
func zlibEncode(str []byte) []byte {
|
func zlibEncode(str []byte) []byte {
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
writer := zlib.NewWriter(&buff)
|
writer := zlib.NewWriter(&buff)
|
||||||
|
14
docker-compose.dev.yml
Normal file
14
docker-compose.dev.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# This file can be used to start up necessary services.
|
||||||
|
# Copy it into the docker-compose.yml:
|
||||||
|
# > cp docker-compose.dev.yml docker-compose.yml
|
||||||
|
# And then run it:
|
||||||
|
# > docker-compose up -d
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:4.0-32bit
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
27
docker-compose.prod.yml
Normal file
27
docker-compose.prod.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# This file can be used to run application in the production environment.
|
||||||
|
# Copy it into the docker-compose.yml:
|
||||||
|
# > cp docker-compose.prod.yml docker-compose.yml
|
||||||
|
# And then run it:
|
||||||
|
# > docker-compose up -d
|
||||||
|
# Service will be listened at the http://localhost
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: elyby/chrly
|
||||||
|
hostname: chrly0
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- ./data/capes:/data/capes
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
CHRLY_SECRET: replace_this_value_in_production
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:4.0-32bit # 32-bit version is recommended to spare some memory
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
12
docker-entrypoint.sh
Executable file
12
docker-entrypoint.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -d /data/capes ]; then
|
||||||
|
mkdir -p /data/capes
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then
|
||||||
|
set -- /usr/local/bin/chrly "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
@ -1,13 +0,0 @@
|
|||||||
FROM alpine:3.6
|
|
||||||
|
|
||||||
RUN apk --update add ca-certificates \
|
|
||||||
&& update-ca-certificates \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
COPY docker/docker-entrypoint.sh /usr/local/bin/
|
|
||||||
COPY docker/config.dist.yml /usr/local/etc/minecraft-skinsystem/
|
|
||||||
|
|
||||||
COPY minecraft-skinsystem /usr/local/bin/
|
|
||||||
|
|
||||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
||||||
CMD ["serve"]
|
|
@ -1,51 +0,0 @@
|
|||||||
# Main server configuration. Actually you don't want to change it,
|
|
||||||
# but you able to change host or port, that will be used by serve command
|
|
||||||
server:
|
|
||||||
host: # leave host empty to allow Docker publish port
|
|
||||||
port: 80
|
|
||||||
|
|
||||||
# Worker listen to AMQP events, so it should know how to connect to any
|
|
||||||
# AMQP provider (actually RabbitMQ). You should not escape any vhost
|
|
||||||
# characters, 'cause it will be done by application automatically
|
|
||||||
amqp:
|
|
||||||
host: rabbitmq
|
|
||||||
port: 5672
|
|
||||||
username: minecraft-skinsystem-app
|
|
||||||
password: minecraft-skinsystem-app-password
|
|
||||||
vhost: /
|
|
||||||
|
|
||||||
# Both of web or worker depends on storage.
|
|
||||||
storage:
|
|
||||||
# For now app require Redis and don't support any other backends to store
|
|
||||||
# skins, but in the future we can have more backends. Poll size tune amount
|
|
||||||
# of connections to the redis. It's not recommended to set it less then 2
|
|
||||||
# because it will lead to panic on high load.
|
|
||||||
redis:
|
|
||||||
host: redis
|
|
||||||
port: 6379
|
|
||||||
poolSize: 10
|
|
||||||
|
|
||||||
# Filesystem storage used to store capes. basePath specify absolute or relative
|
|
||||||
# path to storage and capesDirName specify which folder in this base path will
|
|
||||||
# be used to search capes.
|
|
||||||
filesystem:
|
|
||||||
basePath: /data
|
|
||||||
capesDirName: capes
|
|
||||||
|
|
||||||
# Accounts Ely.by internal API will be used in cases, when by some reasons
|
|
||||||
# information about user will be unavailable in the app storage.
|
|
||||||
api:
|
|
||||||
accounts:
|
|
||||||
host: https://account.ely.by
|
|
||||||
id: app-id
|
|
||||||
secret: secret
|
|
||||||
scopes:
|
|
||||||
- internal_account_info
|
|
||||||
|
|
||||||
# StatsD can be used to collect metrics
|
|
||||||
# statsd:
|
|
||||||
# addr: localhost:3746
|
|
||||||
|
|
||||||
# Sentry can be used to collect app errors
|
|
||||||
# sentry:
|
|
||||||
# dsn: https://public:private@your.sentry.io/1
|
|
@ -1,46 +0,0 @@
|
|||||||
# This compose file contains necessary docker-compose config to quick start
|
|
||||||
# services required by app. Ports published to host.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# 1. Clone this file as docker-compose.yml:
|
|
||||||
# cp docker/docker-compose.dev.yml docker-compose.yml
|
|
||||||
#
|
|
||||||
# 2. If necessary, then you can fix configuration to your environment.
|
|
||||||
# Then start all services:
|
|
||||||
# docker-compose up -d
|
|
||||||
#
|
|
||||||
# 3. Pass to the project configuration links to this services:
|
|
||||||
# amqp:
|
|
||||||
# host: localhost
|
|
||||||
# port: 5672
|
|
||||||
# username: ely
|
|
||||||
# password: ely
|
|
||||||
# vhost: /ely
|
|
||||||
#
|
|
||||||
# storage:
|
|
||||||
# redis:
|
|
||||||
# host: localhost
|
|
||||||
# port: 6379
|
|
||||||
# poolSize: 10
|
|
||||||
#
|
|
||||||
# 4. After job is done all services can be stopped:
|
|
||||||
# docker-compose stop
|
|
||||||
|
|
||||||
version: '2'
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:3.2-32bit
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- ./data/redis:/data
|
|
||||||
|
|
||||||
rabbitmq:
|
|
||||||
image: rabbitmq:3.6-management-alpine
|
|
||||||
ports:
|
|
||||||
- "5672:5672"
|
|
||||||
- "15672:15672"
|
|
||||||
environment:
|
|
||||||
RABBITMQ_DEFAULT_USER: "ely"
|
|
||||||
RABBITMQ_DEFAULT_PASS: "ely"
|
|
||||||
RABBITMQ_DEFAULT_VHOST: "/ely"
|
|
@ -1,36 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
image: registry.ely.by/elyby/skinsystem:latest
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
volumes:
|
|
||||||
- ./data/capes:/data/capes
|
|
||||||
- ./config/minecraft-skinsystem:/etc/minecraft-skinsystem
|
|
||||||
|
|
||||||
worker:
|
|
||||||
image: registry.ely.by/elyby/skinsystem:latest
|
|
||||||
restart: always
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
- rabbitmq
|
|
||||||
command: ["amqp-worker"]
|
|
||||||
volumes:
|
|
||||||
- ./config/minecraft-skinsystem:/etc/minecraft-skinsystem
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:3.2-32bit # 32-bit version used to decrease memory usage
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./data/redis:/data
|
|
||||||
|
|
||||||
rabbitmq:
|
|
||||||
image: rabbitmq:3.6-alpine
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
RABBITMQ_DEFAULT_USER: minecraft-skinsystem-app
|
|
||||||
RABBITMQ_DEFAULT_PASS: minecraft-skinsystem-app-password
|
|
||||||
RABBITMQ_DEFAULT_VHOST: /
|
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CONFIG="/etc/minecraft-skinsystem/config.yml"
|
|
||||||
|
|
||||||
if [ ! -f "$CONFIG" ]; then
|
|
||||||
mkdir -p $(dirname "${CONFIG}")
|
|
||||||
cp /usr/local/etc/minecraft-skinsystem/config.dist.yml "$CONFIG"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$1" = "serve" ] || [ "$1" = "amqp-worker" ]; then
|
|
||||||
set -- minecraft-skinsystem "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
256
http/api.go
Normal file
256
http/api.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/auth"
|
||||||
|
"github.com/elyby/chrly/db"
|
||||||
|
"github.com/elyby/chrly/interfaces"
|
||||||
|
"github.com/elyby/chrly/model"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
"github.com/thedevsaddam/govalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
govalidator.AddCustomRule("md5", func(field string, rule string, message string, value interface{}) error {
|
||||||
|
val := []byte(value.(string))
|
||||||
|
if ok, _ := regexp.Match(`^[a-f0-9]{32}$`, val); !ok {
|
||||||
|
if message == "" {
|
||||||
|
message = fmt.Sprintf("The %s field must be a valid md5 hash", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||||
|
if message == "" {
|
||||||
|
message = "Skin uploading is temporary unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("api.skins.post.request", 1)
|
||||||
|
validationErrors := validatePostSkinRequest(req)
|
||||||
|
if validationErrors != nil {
|
||||||
|
cfg.Logger.IncCounter("api.skins.post.validation_failed", 1)
|
||||||
|
apiBadRequest(resp, validationErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
||||||
|
username := req.Form.Get("username")
|
||||||
|
|
||||||
|
record, err := findIdentity(cfg.SkinsRepo, identityId, username)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.Error("Error on requesting a skin from the repository: :err", wd.ErrParam(err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
||||||
|
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
||||||
|
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
||||||
|
|
||||||
|
record.Uuid = req.Form.Get("uuid")
|
||||||
|
record.SkinId = skinId
|
||||||
|
record.Hash = req.Form.Get("hash")
|
||||||
|
record.Is1_8 = is18
|
||||||
|
record.IsSlim = isSlim
|
||||||
|
record.Url = req.Form.Get("url")
|
||||||
|
record.MojangTextures = req.Form.Get("mojangTextures")
|
||||||
|
record.MojangSignature = req.Form.Get("mojangSignature")
|
||||||
|
|
||||||
|
err = cfg.SkinsRepo.Save(record)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.Error("Unable to save record to the repository: :err", wd.ErrParam(err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger.IncCounter("api.skins.post.success", 1)
|
||||||
|
resp.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("api.skins.delete.request", 1)
|
||||||
|
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
||||||
|
skin, err := cfg.SkinsRepo.FindByUserId(id)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.IncCounter("api.skins.delete.not_found", 1)
|
||||||
|
apiNotFound(resp, "Cannot find record for requested user id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.deleteSkin(skin, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("api.skins.delete.request", 1)
|
||||||
|
username := mux.Vars(req)["username"]
|
||||||
|
skin, err := cfg.SkinsRepo.FindByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.IncCounter("api.skins.delete.not_found", 1)
|
||||||
|
apiNotFound(resp, "Cannot find record for requested username")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.deleteSkin(skin, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Authenticate(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
cfg.Logger.IncCounter("authentication.challenge", 1)
|
||||||
|
err := cfg.Auth.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*auth.Unauthorized); ok {
|
||||||
|
cfg.Logger.IncCounter("authentication.failed", 1)
|
||||||
|
apiForbidden(resp, err.Error())
|
||||||
|
} else {
|
||||||
|
cfg.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err))
|
||||||
|
apiServerError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger.IncCounter("authentication.success", 1)
|
||||||
|
handler.ServeHTTP(resp, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) deleteSkin(skin *model.Skin, resp http.ResponseWriter) {
|
||||||
|
err := cfg.SkinsRepo.RemoveByUserId(skin.UserId)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.Error("Cannot delete skin by error: :err", wd.ErrParam(err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger.IncCounter("api.skins.delete.success", 1)
|
||||||
|
resp.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||||
|
const maxMultipartMemory int64 = 32 << 20
|
||||||
|
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||||
|
|
||||||
|
request.ParseMultipartForm(maxMultipartMemory)
|
||||||
|
|
||||||
|
validationRules := govalidator.MapData{
|
||||||
|
"identityId": {"required", "numeric", "min:1"},
|
||||||
|
"username": {"required"},
|
||||||
|
"uuid": {"required", "uuid"},
|
||||||
|
"skinId": {"required", "numeric", "min:1"},
|
||||||
|
"url": {"url"},
|
||||||
|
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||||
|
"hash": {"md5"},
|
||||||
|
"is1_8": {"bool"},
|
||||||
|
"isSlim": {"bool"},
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAppendSkinRequiredError := false
|
||||||
|
url := request.Form.Get("url")
|
||||||
|
_, _, skinErr := request.FormFile("skin")
|
||||||
|
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||||
|
shouldAppendSkinRequiredError = true
|
||||||
|
} else if skinErr == nil {
|
||||||
|
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||||
|
} else if url != "" {
|
||||||
|
validationRules["hash"] = append(validationRules["hash"], "required")
|
||||||
|
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||||
|
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mojangTextures := request.Form.Get("mojangTextures")
|
||||||
|
if mojangTextures != "" {
|
||||||
|
validationRules["mojangSignature"] = []string{"required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := govalidator.New(govalidator.Options{
|
||||||
|
Request: request,
|
||||||
|
Rules: validationRules,
|
||||||
|
RequiredDefault: false,
|
||||||
|
FormSize: maxMultipartMemory,
|
||||||
|
})
|
||||||
|
validationResults := validator.Validate()
|
||||||
|
if shouldAppendSkinRequiredError {
|
||||||
|
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||||
|
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validationResults) != 0 {
|
||||||
|
return validationResults
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIdentity(repo interfaces.SkinsRepository, identityId int, username string) (*model.Skin, error) {
|
||||||
|
var record *model.Skin
|
||||||
|
record, err := repo.FindByUserId(identityId)
|
||||||
|
if err != nil {
|
||||||
|
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err = repo.FindByUsername(username)
|
||||||
|
if err == nil {
|
||||||
|
repo.RemoveByUsername(username)
|
||||||
|
record.UserId = identityId
|
||||||
|
} else {
|
||||||
|
record = &model.Skin{
|
||||||
|
UserId: identityId,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if record.Username != username {
|
||||||
|
repo.RemoveByUserId(identityId)
|
||||||
|
record.Username = username
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) {
|
||||||
|
resp.WriteHeader(http.StatusBadRequest)
|
||||||
|
resp.Header().Set("Content-Type", "application/json")
|
||||||
|
result, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"errors": errorsPerField,
|
||||||
|
})
|
||||||
|
resp.Write(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiForbidden(resp http.ResponseWriter, reason string) {
|
||||||
|
resp.WriteHeader(http.StatusForbidden)
|
||||||
|
resp.Header().Set("Content-Type", "application/json")
|
||||||
|
result, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"error": reason,
|
||||||
|
})
|
||||||
|
resp.Write(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiNotFound(resp http.ResponseWriter, reason string) {
|
||||||
|
resp.WriteHeader(http.StatusNotFound)
|
||||||
|
resp.Header().Set("Content-Type", "application/json")
|
||||||
|
result, _ := json.Marshal([]interface{}{
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
resp.Write(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiServerError(resp http.ResponseWriter) {
|
||||||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
505
http/api_test.go
Normal file
505
http/api_test.go
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/auth"
|
||||||
|
"github.com/elyby/chrly/db"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_Valid(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
resultModel := createSkinModel("mock_user", false)
|
||||||
|
resultModel.SkinId = 5
|
||||||
|
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||||
|
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||||
|
resultModel.MojangTextures = ""
|
||||||
|
resultModel.MojangSignature = ""
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||||
|
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"mock_user"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(201, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
resultModel := createSkinModel("mock_user", false)
|
||||||
|
resultModel.UserId = 2
|
||||||
|
resultModel.SkinId = 5
|
||||||
|
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||||
|
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||||
|
resultModel.MojangTextures = ""
|
||||||
|
resultModel.MojangSignature = ""
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"identityId": {"2"},
|
||||||
|
"username": {"mock_user"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||||
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil)
|
||||||
|
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(201, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_ChangedUsername(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
resultModel := createSkinModel("changed_username", false)
|
||||||
|
resultModel.SkinId = 5
|
||||||
|
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||||
|
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||||
|
resultModel.MojangTextures = ""
|
||||||
|
resultModel.MojangSignature = ""
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"changed_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||||
|
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(201, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
resultModel := createSkinModel("mock_user", false)
|
||||||
|
resultModel.SkinId = 5
|
||||||
|
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||||
|
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||||
|
resultModel.MojangTextures = ""
|
||||||
|
resultModel.MojangSignature = ""
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"mock_user"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||||
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||||
|
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(201, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_UploadSkin(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||||
|
part.Write(loadSkinFile())
|
||||||
|
|
||||||
|
_ = writer.WriteField("identityId", "1")
|
||||||
|
_ = writer.WriteField("username", "mock_user")
|
||||||
|
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||||
|
_ = writer.WriteField("skinId", "5")
|
||||||
|
|
||||||
|
err := writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", body)
|
||||||
|
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(400, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"errors": {
|
||||||
|
"skin": [
|
||||||
|
"Skin uploading is temporary unavailable"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_RequiredFields(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
form := url.Values{
|
||||||
|
"hash": {"this is not md5"},
|
||||||
|
"mojangTextures": {"someBase64EncodedString"},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(400, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"errors": {
|
||||||
|
"identityId": [
|
||||||
|
"The identityId field is required",
|
||||||
|
"The identityId field must be numeric",
|
||||||
|
"The identityId field must be minimum 1 char"
|
||||||
|
],
|
||||||
|
"skinId": [
|
||||||
|
"The skinId field is required",
|
||||||
|
"The skinId field must be numeric",
|
||||||
|
"The skinId field must be minimum 1 char"
|
||||||
|
],
|
||||||
|
"username": [
|
||||||
|
"The username field is required"
|
||||||
|
],
|
||||||
|
"uuid": [
|
||||||
|
"The uuid field is required",
|
||||||
|
"The uuid field must contain valid UUID"
|
||||||
|
],
|
||||||
|
"hash": [
|
||||||
|
"The hash field must be a valid md5 hash"
|
||||||
|
],
|
||||||
|
"url": [
|
||||||
|
"One of url or skin should be provided, but not both"
|
||||||
|
],
|
||||||
|
"skin": [
|
||||||
|
"One of url or skin should be provided, but not both"
|
||||||
|
],
|
||||||
|
"mojangSignature": [
|
||||||
|
"The mojangSignature field is required"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_PostSkin_Unauthorized(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", nil)
|
||||||
|
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"Cannot parse passed JWT token"})
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(403, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"error": "Cannot parse passed JWT token"
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DeleteSkinByUserId_Success(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||||
|
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(204, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DeleteSkinByUserId_NotFound(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:2", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(404, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`[
|
||||||
|
"Cannot find record for requested user id"
|
||||||
|
]`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DeleteSkinByUsername_Success(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
|
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(204, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.Empty(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user_2", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||||
|
mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{"mock_user_2"})
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
|
||||||
|
|
||||||
|
config.CreateHandler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(404, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`[
|
||||||
|
"Cannot find record for requested username"
|
||||||
|
]`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"signing key not available"})
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||||
|
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
|
||||||
|
|
||||||
|
res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {}))
|
||||||
|
res.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(403, resp.StatusCode)
|
||||||
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.JSONEq(`{
|
||||||
|
"error": "signing key not available"
|
||||||
|
}`, string(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
||||||
|
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
||||||
|
|
||||||
|
func loadSkinFile() []byte {
|
||||||
|
result := make([]byte, 92)
|
||||||
|
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@ -11,8 +11,8 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
testify "github.com/stretchr/testify/assert"
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
"github.com/elyby/chrly/db"
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Cape(t *testing.T) {
|
func TestConfig_Cape(t *testing.T) {
|
||||||
@ -21,14 +21,14 @@ func TestConfig_Cape(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, _, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
cape := createCape()
|
cape := createCape()
|
||||||
|
|
||||||
capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||||
File: bytes.NewReader(cape),
|
File: bytes.NewReader(cape),
|
||||||
}, nil)
|
}, nil)
|
||||||
wd.EXPECT().IncCounter("capes.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("capes.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -48,10 +48,10 @@ func TestConfig_Cape2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, _, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||||
wd.EXPECT().IncCounter("capes.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("capes.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -69,15 +69,15 @@ func TestConfig_CapeGET(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, _, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
cape := createCape()
|
cape := createCape()
|
||||||
|
|
||||||
capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||||
File: bytes.NewReader(cape),
|
File: bytes.NewReader(cape),
|
||||||
}, nil)
|
}, nil)
|
||||||
wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||||
wd.EXPECT().IncCounter("capes.get_request", int64(1))
|
mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -97,11 +97,11 @@ func TestConfig_CapeGET2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, _, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||||
wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||||
wd.EXPECT().IncCounter("capes.get_request", int64(1))
|
mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
27
http/face.go
27
http/face.go
@ -1,27 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultHash = "default"
|
|
||||||
|
|
||||||
func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) {
|
|
||||||
cfg.Logger.IncCounter("faces.request", 1)
|
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
|
||||||
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
|
||||||
var hash string
|
|
||||||
if err != nil || rec.SkinId == 0 {
|
|
||||||
hash = defaultHash
|
|
||||||
} else {
|
|
||||||
hash = rec.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(response, request, buildElyUrl(buildFaceUrl(hash)), 301)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFaceUrl(hash string) string {
|
|
||||||
return "/minecraft/skin_buffer/faces/" + hash + ".png"
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
testify "github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig_Face(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
|
||||||
wd.EXPECT().IncCounter("faces.request", int64(1))
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
config.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
assert.Equal(301, resp.StatusCode)
|
|
||||||
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/55d2a8848764f5ff04012cdb093458bd.png", resp.Header.Get("Location"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_Face2(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"})
|
|
||||||
wd.EXPECT().IncCounter("faces.request", int64(1))
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
config.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
assert.Equal(301, resp.StatusCode)
|
|
||||||
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/default.png", resp.Header.Get("Location"))
|
|
||||||
}
|
|
18
http/http.go
18
http/http.go
@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/mono83/slf/wd"
|
"github.com/mono83/slf/wd"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/interfaces"
|
"github.com/elyby/chrly/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -22,6 +22,7 @@ type Config struct {
|
|||||||
SkinsRepo interfaces.SkinsRepository
|
SkinsRepo interfaces.SkinsRepository
|
||||||
CapesRepo interfaces.CapesRepository
|
CapesRepo interfaces.CapesRepository
|
||||||
Logger wd.Watchdog
|
Logger wd.Watchdog
|
||||||
|
Auth interfaces.AuthChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) Run() error {
|
func (cfg *Config) Run() error {
|
||||||
@ -54,11 +55,13 @@ func (cfg *Config) CreateHandler() http.Handler {
|
|||||||
router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks")
|
router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks")
|
||||||
router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET")
|
router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET")
|
||||||
router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET")
|
router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET")
|
||||||
router.HandleFunc("/skins/{username}/face", cfg.Face).Methods("GET")
|
|
||||||
router.HandleFunc("/skins/{username}/face.png", cfg.Face).Methods("GET")
|
|
||||||
// Legacy
|
// Legacy
|
||||||
router.HandleFunc("/skins", cfg.SkinGET).Methods("GET")
|
router.HandleFunc("/skins", cfg.SkinGET).Methods("GET")
|
||||||
router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET")
|
router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET")
|
||||||
|
// API
|
||||||
|
router.Handle("/api/skins", cfg.Authenticate(http.HandlerFunc(cfg.PostSkin))).Methods("POST")
|
||||||
|
router.Handle("/api/skins/id:{id:[0-9]+}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUserId))).Methods("DELETE")
|
||||||
|
router.Handle("/api/skins/{username}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUsername))).Methods("DELETE")
|
||||||
// 404
|
// 404
|
||||||
router.NotFoundHandler = http.HandlerFunc(cfg.NotFound)
|
router.NotFoundHandler = http.HandlerFunc(cfg.NotFound)
|
||||||
|
|
||||||
@ -74,15 +77,6 @@ func parseUsername(username string) string {
|
|||||||
return username
|
return username
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildElyUrl(route string) string {
|
|
||||||
prefix := "http://ely.by"
|
|
||||||
if !strings.HasPrefix(route, prefix) {
|
|
||||||
route = prefix + route
|
|
||||||
}
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForSignal() os.Signal {
|
func waitForSignal() os.Signal {
|
||||||
ch := make(chan os.Signal)
|
ch := make(chan os.Signal)
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
testify "github.com/stretchr/testify/assert"
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/interfaces/mock_interfaces"
|
"github.com/elyby/chrly/interfaces/mock_interfaces"
|
||||||
"elyby/minecraft-skinsystem/interfaces/mock_wd"
|
"github.com/elyby/chrly/interfaces/mock_wd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseUsername(t *testing.T) {
|
func TestParseUsername(t *testing.T) {
|
||||||
@ -16,25 +16,31 @@ func TestParseUsername(t *testing.T) {
|
|||||||
assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end")
|
assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildElyUrl(t *testing.T) {
|
type mocks struct {
|
||||||
assert := testify.New(t)
|
Skins *mock_interfaces.MockSkinsRepository
|
||||||
assert.Equal("http://ely.by/route", buildElyUrl("/route"), "Function should add prefix to the provided relative url.")
|
Capes *mock_interfaces.MockCapesRepository
|
||||||
assert.Equal("http://ely.by/test/route", buildElyUrl("http://ely.by/test/route"), "Function should do not add prefix to the provided prefixed url.")
|
Auth *mock_interfaces.MockAuthChecker
|
||||||
|
Log *mock_wd.MockWatchdog
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupMocks(ctrl *gomock.Controller) (
|
func setupMocks(ctrl *gomock.Controller) (
|
||||||
*Config,
|
*Config,
|
||||||
*mock_interfaces.MockSkinsRepository,
|
*mocks,
|
||||||
*mock_interfaces.MockCapesRepository,
|
|
||||||
*mock_wd.MockWatchdog,
|
|
||||||
) {
|
) {
|
||||||
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
||||||
capesRepo := mock_interfaces.NewMockCapesRepository(ctrl)
|
capesRepo := mock_interfaces.NewMockCapesRepository(ctrl)
|
||||||
|
authChecker := mock_interfaces.NewMockAuthChecker(ctrl)
|
||||||
wd := mock_wd.NewMockWatchdog(ctrl)
|
wd := mock_wd.NewMockWatchdog(ctrl)
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
SkinsRepo: skinsRepo,
|
SkinsRepo: skinsRepo,
|
||||||
CapesRepo: capesRepo,
|
CapesRepo: capesRepo,
|
||||||
Logger: wd,
|
Auth: authChecker,
|
||||||
}, skinsRepo, capesRepo, wd
|
Logger: wd,
|
||||||
|
}, &mocks{
|
||||||
|
Skins: skinsRepo,
|
||||||
|
Capes: capesRepo,
|
||||||
|
Auth: authChecker,
|
||||||
|
Log: wd,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) {
|
func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) {
|
||||||
data, _ := json.Marshal(map[string]string{
|
data, _ := json.Marshal(map[string]string{
|
||||||
"status": "404",
|
"status": "404",
|
||||||
"message": "Not Found",
|
"message": "Not Found",
|
||||||
"link": "http://docs.ely.by/skin-system.html",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
response.Header().Set("Content-Type", "application/json")
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -22,7 +22,6 @@ func TestConfig_NotFound(t *testing.T) {
|
|||||||
response, _ := ioutil.ReadAll(resp.Body)
|
response, _ := ioutil.ReadAll(resp.Body)
|
||||||
assert.JSONEq(`{
|
assert.JSONEq(`{
|
||||||
"status": "404",
|
"status": "404",
|
||||||
"message": "Not Found",
|
"message": "Not Found"
|
||||||
"link": "http://docs.ely.by/skin-system.html"
|
|
||||||
}`, string(response))
|
}`, string(response))
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
type signedTexturesResponse struct {
|
type signedTexturesResponse struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IsEly bool `json:"ely,omitempty"`
|
|
||||||
Props []property `json:"properties"`
|
Props []property `json:"properties"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +40,8 @@ func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Re
|
|||||||
Value: rec.MojangTextures,
|
Value: rec.MojangTextures,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "ely",
|
Name: "chrly",
|
||||||
Value: "but why are you asking?",
|
Value: "how do you tame a horse in Minecraft?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
testify "github.com/stretchr/testify/assert"
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
"github.com/elyby/chrly/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_SignedTextures(t *testing.T) {
|
func TestConfig_SignedTextures(t *testing.T) {
|
||||||
@ -17,10 +17,10 @@ func TestConfig_SignedTextures(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
wd.EXPECT().IncCounter("signed_textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -41,8 +41,8 @@ func TestConfig_SignedTextures(t *testing.T) {
|
|||||||
"value": "mocked textures base64"
|
"value": "mocked textures base64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ely",
|
"name": "chrly",
|
||||||
"value": "but why are you asking?"
|
"value": "how do you tame a horse in Minecraft?"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`, string(response))
|
}`, string(response))
|
||||||
@ -54,10 +54,10 @@ func TestConfig_SignedTextures2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{})
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{})
|
||||||
wd.EXPECT().IncCounter("signed_textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -18,7 +18,7 @@ func (cfg *Config) Skin(response http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(response, request, buildElyUrl(rec.Url), 301)
|
http.Redirect(response, request, rec.Url, 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) {
|
func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) {
|
||||||
|
@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
testify "github.com/stretchr/testify/assert"
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
"github.com/elyby/chrly/db"
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Skin(t *testing.T) {
|
func TestConfig_Skin(t *testing.T) {
|
||||||
@ -17,10 +17,10 @@ func TestConfig_Skin(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
wd.EXPECT().IncCounter("skins.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("skins.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -38,10 +38,10 @@ func TestConfig_Skin2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||||
wd.EXPECT().IncCounter("skins.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("skins.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -59,11 +59,11 @@ func TestConfig_SkinGET(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
wd.EXPECT().IncCounter("skins.get_request", int64(1))
|
mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||||
wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -81,11 +81,11 @@ func TestConfig_SkinGET2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, _, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||||
wd.EXPECT().IncCounter("skins.get_request", int64(1))
|
mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||||
wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -112,6 +112,7 @@ func TestConfig_SkinGET3(t *testing.T) {
|
|||||||
|
|
||||||
func createSkinModel(username string, isSlim bool) *model.Skin {
|
func createSkinModel(username string, isSlim bool) *model.Skin {
|
||||||
return &model.Skin{
|
return &model.Skin{
|
||||||
|
UserId: 1,
|
||||||
Username: username,
|
Username: username,
|
||||||
Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||||
SkinId: 1,
|
SkinId: 1,
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type texturesResponse struct {
|
type texturesResponse struct {
|
||||||
@ -46,8 +46,6 @@ func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request)
|
|||||||
|
|
||||||
skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
||||||
skin.Hash = string(buildNonElyTexturesHash(username))
|
skin.Hash = string(buildNonElyTexturesHash(username))
|
||||||
} else {
|
|
||||||
skin.Url = buildElyUrl(skin.Url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textures := texturesResponse{
|
textures := texturesResponse{
|
||||||
|
@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
testify "github.com/stretchr/testify/assert"
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
"github.com/elyby/chrly/db"
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Textures(t *testing.T) {
|
func TestConfig_Textures(t *testing.T) {
|
||||||
@ -20,11 +20,11 @@ func TestConfig_Textures(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||||
wd.EXPECT().IncCounter("textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -49,11 +49,11 @@ func TestConfig_Textures2(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil)
|
||||||
capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||||
wd.EXPECT().IncCounter("textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -81,13 +81,13 @@ func TestConfig_Textures3(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||||
capesRepo.EXPECT().FindByUsername("mock_user").Return(&model.Cape{
|
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(&model.Cape{
|
||||||
File: bytes.NewReader(createCape()),
|
File: bytes.NewReader(createCape()),
|
||||||
}, nil)
|
}, nil)
|
||||||
wd.EXPECT().IncCounter("textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -116,11 +116,11 @@ func TestConfig_Textures4(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
config, skinsRepo, capesRepo, wd := setupMocks(ctrl)
|
config, mocks := setupMocks(ctrl)
|
||||||
|
|
||||||
skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{})
|
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{})
|
||||||
capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{})
|
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{})
|
||||||
wd.EXPECT().IncCounter("textures.request", int64(1))
|
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||||
timeNow = func() time.Time {
|
timeNow = func() time.Time {
|
||||||
return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC)
|
return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package interfaces
|
|
||||||
|
|
||||||
import (
|
|
||||||
"elyby/minecraft-skinsystem/api/accounts"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountsAPI interface {
|
|
||||||
AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error)
|
|
||||||
}
|
|
7
interfaces/auth.go
Normal file
7
interfaces/auth.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type AuthChecker interface {
|
||||||
|
Check(req *http.Request) error
|
||||||
|
}
|
@ -1,46 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: interfaces/api.go
|
|
||||||
|
|
||||||
package mock_interfaces
|
|
||||||
|
|
||||||
import (
|
|
||||||
accounts "elyby/minecraft-skinsystem/api/accounts"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
reflect "reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockAccountsAPI is a mock of AccountsAPI interface
|
|
||||||
type MockAccountsAPI struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockAccountsAPIMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAccountsAPIMockRecorder is the mock recorder for MockAccountsAPI
|
|
||||||
type MockAccountsAPIMockRecorder struct {
|
|
||||||
mock *MockAccountsAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockAccountsAPI creates a new mock instance
|
|
||||||
func NewMockAccountsAPI(ctrl *gomock.Controller) *MockAccountsAPI {
|
|
||||||
mock := &MockAccountsAPI{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockAccountsAPIMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
|
||||||
func (_m *MockAccountsAPI) EXPECT() *MockAccountsAPIMockRecorder {
|
|
||||||
return _m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountInfo mocks base method
|
|
||||||
func (_m *MockAccountsAPI) AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error) {
|
|
||||||
ret := _m.ctrl.Call(_m, "AccountInfo", attribute, value)
|
|
||||||
ret0, _ := ret[0].(*accounts.AccountInfoResponse)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountInfo indicates an expected call of AccountInfo
|
|
||||||
func (_mr *MockAccountsAPIMockRecorder) AccountInfo(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "AccountInfo", reflect.TypeOf((*MockAccountsAPI)(nil).AccountInfo), arg0, arg1)
|
|
||||||
}
|
|
45
interfaces/mock_interfaces/mock_auth.go
Normal file
45
interfaces/mock_interfaces/mock_auth.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: interfaces/auth.go
|
||||||
|
|
||||||
|
package mock_interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
http "net/http"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAuthChecker is a mock of AuthChecker interface
|
||||||
|
type MockAuthChecker struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockAuthCheckerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAuthCheckerMockRecorder is the mock recorder for MockAuthChecker
|
||||||
|
type MockAuthCheckerMockRecorder struct {
|
||||||
|
mock *MockAuthChecker
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockAuthChecker creates a new mock instance
|
||||||
|
func NewMockAuthChecker(ctrl *gomock.Controller) *MockAuthChecker {
|
||||||
|
mock := &MockAuthChecker{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockAuthCheckerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (_m *MockAuthChecker) EXPECT() *MockAuthCheckerMockRecorder {
|
||||||
|
return _m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mocks base method
|
||||||
|
func (_m *MockAuthChecker) Check(req *http.Request) error {
|
||||||
|
ret := _m.ctrl.Call(_m, "Check", req)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check indicates an expected call of Check
|
||||||
|
func (_mr *MockAuthCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Check", reflect.TypeOf((*MockAuthChecker)(nil).Check), arg0)
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
package mock_interfaces
|
package mock_interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
model "elyby/minecraft-skinsystem/model"
|
model "github.com/elyby/chrly/model"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
)
|
)
|
||||||
@ -70,6 +70,30 @@ func (_mr *MockSkinsRepositoryMockRecorder) Save(arg0 interface{}) *gomock.Call
|
|||||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0)
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveByUserId mocks base method
|
||||||
|
func (_m *MockSkinsRepository) RemoveByUserId(id int) error {
|
||||||
|
ret := _m.ctrl.Call(_m, "RemoveByUserId", id)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveByUserId indicates an expected call of RemoveByUserId
|
||||||
|
func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUserId(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUserId), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveByUsername mocks base method
|
||||||
|
func (_m *MockSkinsRepository) RemoveByUsername(username string) error {
|
||||||
|
ret := _m.ctrl.Call(_m, "RemoveByUsername", username)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveByUsername indicates an expected call of RemoveByUsername
|
||||||
|
func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUsername(arg0 interface{}) *gomock.Call {
|
||||||
|
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUsername), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// MockCapesRepository is a mock of CapesRepository interface
|
// MockCapesRepository is a mock of CapesRepository interface
|
||||||
type MockCapesRepository struct {
|
type MockCapesRepository struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"elyby/minecraft-skinsystem/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SkinsRepository interface {
|
type SkinsRepository interface {
|
||||||
FindByUsername(username string) (*model.Skin, error)
|
FindByUsername(username string) (*model.Skin, error)
|
||||||
FindByUserId(id int) (*model.Skin, error)
|
FindByUserId(id int) (*model.Skin, error)
|
||||||
Save(skin *model.Skin) error
|
Save(skin *model.Skin) error
|
||||||
|
RemoveByUserId(id int) error
|
||||||
|
RemoveByUsername(username string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type CapesRepository interface {
|
type CapesRepository interface {
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
package sentry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
"github.com/mono83/slf"
|
|
||||||
"github.com/mono83/slf/filters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config holds information for filtered receiver
|
|
||||||
type Config struct {
|
|
||||||
MinLevel string
|
|
||||||
ParamsWhiteList []string
|
|
||||||
ParamsBlackList []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReceiver allows you to create a new receiver in the Sentry
|
|
||||||
// using the fastest and easiest way.
|
|
||||||
// The Config parameter can be passed as nil if you do not need additional filtration.
|
|
||||||
func NewReceiver(dsn string, cfg *Config) (slf.Receiver, error) {
|
|
||||||
client, err := raven.New(dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewReceiverWithCustomRaven(client, cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReceiverWithCustomRaven allows you to create a new receiver in the Sentry
|
|
||||||
// configuring raven.Client by yourself. This can be useful if you need to set
|
|
||||||
// additional parameters, such as release and environment, that will be sent
|
|
||||||
// with each Packet in the Sentry:
|
|
||||||
//
|
|
||||||
// client, err := raven.New("https://some:sentry@dsn.sentry.io/1")
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// client.SetRelease("1.3.2")
|
|
||||||
// client.SetEnvironment("production")
|
|
||||||
// client.SetDefaultLoggerName("sentry-watchdog-receiver")
|
|
||||||
//
|
|
||||||
// sentryReceiver, err := sentry.NewReceiverWithCustomRaven(client, &sentry.Config{
|
|
||||||
// MinLevel: "warn",
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// The Config parameter allows you to add additional filtering, such as the minimum
|
|
||||||
// message level and the exclusion of private parameters. If you do not need additional
|
|
||||||
// filtering, nil can passed.
|
|
||||||
func NewReceiverWithCustomRaven(client *raven.Client, cfg *Config) (slf.Receiver, error) {
|
|
||||||
out, err := buildReceiverForClient(client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg == nil {
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolving level
|
|
||||||
level, ok := slf.ParseType(cfg.MinLevel)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("Unknown level %s", cfg.MinLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.ParamsWhiteList) > 0 {
|
|
||||||
out.filter = slf.NewWhiteListParamsFilter(cfg.ParamsWhiteList)
|
|
||||||
} else {
|
|
||||||
out.filter = slf.NewBlackListParamsFilter(cfg.ParamsBlackList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters.MinLogLevel(level, out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildReceiverForClient(client *raven.Client) (*sentryLogReceiver, error) {
|
|
||||||
return &sentryLogReceiver{target: client, filter: slf.NewBlackListParamsFilter(nil)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sentryLogReceiver struct {
|
|
||||||
target *raven.Client
|
|
||||||
filter slf.ParamsFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l sentryLogReceiver) Receive(p slf.Event) {
|
|
||||||
if !p.IsLog() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt := raven.NewPacket(
|
|
||||||
slf.ReplacePlaceholders(p.Content, p.Params, false),
|
|
||||||
// First 5 means, that first N elements will be skipped before actual app trace
|
|
||||||
// This is needed to exclude watchdog calls from stack trace
|
|
||||||
raven.NewStacktrace(5, 5, []string{}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(p.Params) > 0 {
|
|
||||||
shownParams := l.filter(p.Params)
|
|
||||||
for _, param := range shownParams {
|
|
||||||
value := param.GetRaw()
|
|
||||||
if e, ok := value.(error); ok && e != nil {
|
|
||||||
value = e.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt.Extra[param.GetKey()] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt.Level = convertType(p.Type)
|
|
||||||
pkt.Timestamp = raven.Timestamp(p.Time)
|
|
||||||
|
|
||||||
l.target.Capture(pkt, map[string]string{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertType(wdType byte) raven.Severity {
|
|
||||||
switch wdType {
|
|
||||||
case slf.TypeTrace:
|
|
||||||
case slf.TypeDebug:
|
|
||||||
return raven.DEBUG
|
|
||||||
case slf.TypeInfo:
|
|
||||||
return raven.INFO
|
|
||||||
case slf.TypeWarning:
|
|
||||||
return raven.WARNING
|
|
||||||
case slf.TypeError:
|
|
||||||
return raven.ERROR
|
|
||||||
case slf.TypeAlert:
|
|
||||||
case slf.TypeEmergency:
|
|
||||||
return raven.FATAL
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("Unknown wd type " + string(wdType))
|
|
||||||
}
|
|
2
main.go
2
main.go
@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/cmd"
|
"github.com/elyby/chrly/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
4
script/mocks
Executable file
4
script/mocks
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
mockgen -source=interfaces/repositories.go -destination=interfaces/mock_interfaces/mock_interfaces.go
|
||||||
|
mockgen -source=interfaces/auth.go -destination=interfaces/mock_interfaces/mock_auth.go
|
@ -1,187 +0,0 @@
|
|||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
testify "github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/api/accounts"
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
|
||||||
"elyby/minecraft-skinsystem/interfaces/mock_interfaces"
|
|
||||||
"elyby/minecraft-skinsystem/interfaces/mock_wd"
|
|
||||||
"elyby/minecraft-skinsystem/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestServices_HandleChangeUsername(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
services, skinRepo, _, wd := setupMocks(ctrl)
|
|
||||||
|
|
||||||
resultModel := createSourceModel()
|
|
||||||
resultModel.Username = "new_username"
|
|
||||||
|
|
||||||
// Запись о скине существует, никаких осложнений
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil)
|
|
||||||
skinRepo.EXPECT().Save(resultModel)
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
|
||||||
|
|
||||||
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
|
||||||
AccountId: 1,
|
|
||||||
OldUsername: "mock_user",
|
|
||||||
NewUsername: "new_username",
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Событие с пустым ником, т.е это регистрация, так что нужно создать запись о скине
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Times(0)
|
|
||||||
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock"})
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username_empty_old_username", int64(1))
|
|
||||||
|
|
||||||
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
|
||||||
AccountId: 1,
|
|
||||||
OldUsername: "",
|
|
||||||
NewUsername: "new_mock",
|
|
||||||
}))
|
|
||||||
|
|
||||||
// В базе системы скинов нет записи об указанном пользователе, так что её нужно восстановить
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{})
|
|
||||||
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"})
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1))
|
|
||||||
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
|
||||||
|
|
||||||
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
|
||||||
AccountId: 1,
|
|
||||||
OldUsername: "mock_user",
|
|
||||||
NewUsername: "new_mock2",
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Репозиторий вернул неожиданную ошибку
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mock error"))
|
|
||||||
skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"})
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1))
|
|
||||||
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
|
||||||
wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any())
|
|
||||||
|
|
||||||
assert.True(services.HandleChangeUsername(&UsernameChanged{
|
|
||||||
AccountId: 1,
|
|
||||||
OldUsername: "mock_user",
|
|
||||||
NewUsername: "new_mock2",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServices_HandleSkinChanged(t *testing.T) {
|
|
||||||
assert := testify.New(t)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
services, skinRepo, accountsAPI, wd := setupMocks(ctrl)
|
|
||||||
|
|
||||||
event := &SkinChanged{
|
|
||||||
AccountId: 1,
|
|
||||||
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
|
||||||
SkinId: 2,
|
|
||||||
OldSkinId: 1,
|
|
||||||
Hash: "f76caa016e07267a05b7daf9ebc7419c",
|
|
||||||
Is1_8: true,
|
|
||||||
IsSlim: false,
|
|
||||||
Url: "http://ely.by/minecraft/skins/69c6740d2993e5d6f6a7fc92420efc29.png",
|
|
||||||
MojangTextures: "new mocked textures base64",
|
|
||||||
MojangSignature: "new mocked signature",
|
|
||||||
}
|
|
||||||
|
|
||||||
resultModel := createSourceModel()
|
|
||||||
resultModel.SkinId = event.SkinId
|
|
||||||
resultModel.Hash = event.Hash
|
|
||||||
resultModel.Is1_8 = event.Is1_8
|
|
||||||
resultModel.IsSlim = event.IsSlim
|
|
||||||
resultModel.Url = event.Url
|
|
||||||
resultModel.MojangTextures = event.MojangTextures
|
|
||||||
resultModel.MojangSignature = event.MojangSignature
|
|
||||||
|
|
||||||
// Запись о скине существует, никаких осложнений
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil)
|
|
||||||
skinRepo.EXPECT().Save(resultModel)
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
|
||||||
|
|
||||||
assert.True(services.HandleSkinChanged(event))
|
|
||||||
|
|
||||||
// Записи о скине не существует, она должна быть восстановлена
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"})
|
|
||||||
skinRepo.EXPECT().Save(resultModel)
|
|
||||||
accountsAPI.EXPECT().AccountInfo("id", "1").Return(&accounts.AccountInfoResponse{
|
|
||||||
Id: 1,
|
|
||||||
Username: "mock_user",
|
|
||||||
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
|
||||||
Email: "mock-user@ely.by",
|
|
||||||
}, nil)
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_restored", int64(1))
|
|
||||||
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
|
||||||
wd.EXPECT().Info("User info successfully restored.")
|
|
||||||
|
|
||||||
assert.True(services.HandleSkinChanged(event))
|
|
||||||
|
|
||||||
// Записи о скине не существует, и Ely.by Accounts internal API не знает о таком пользователе
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"})
|
|
||||||
accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{})
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1))
|
|
||||||
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
|
||||||
wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any())
|
|
||||||
|
|
||||||
assert.True(services.HandleSkinChanged(event))
|
|
||||||
|
|
||||||
// Репозиторий скинов вернул неизвестную ошибку, и Ely.by Accounts internal API не знает о таком пользователе
|
|
||||||
skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mocked error"))
|
|
||||||
accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{})
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1))
|
|
||||||
wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1))
|
|
||||||
wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any())
|
|
||||||
wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any())
|
|
||||||
wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any())
|
|
||||||
|
|
||||||
assert.True(services.HandleSkinChanged(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createSourceModel() *model.Skin {
|
|
||||||
return &model.Skin{
|
|
||||||
UserId: 1,
|
|
||||||
Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623",
|
|
||||||
Username: "mock_user",
|
|
||||||
SkinId: 1,
|
|
||||||
Url: "http://ely.by/minecraft/skins/3a345c701f473ac08c8c5b8ecb58ecf3.png",
|
|
||||||
Is1_8: false,
|
|
||||||
IsSlim: false,
|
|
||||||
Hash: "3a345c701f473ac08c8c5b8ecb58ecf3",
|
|
||||||
MojangTextures: "mocked textures base64",
|
|
||||||
MojangSignature: "mocked signature",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupMocks(ctrl *gomock.Controller) (
|
|
||||||
*Services,
|
|
||||||
*mock_interfaces.MockSkinsRepository,
|
|
||||||
*mock_interfaces.MockAccountsAPI,
|
|
||||||
*mock_wd.MockWatchdog,
|
|
||||||
) {
|
|
||||||
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
|
||||||
accountApi := mock_interfaces.NewMockAccountsAPI(ctrl)
|
|
||||||
wd := mock_wd.NewMockWatchdog(ctrl)
|
|
||||||
|
|
||||||
return &Services{
|
|
||||||
SkinsRepo: skinsRepo,
|
|
||||||
AccountsAPI: accountApi,
|
|
||||||
Logger: wd,
|
|
||||||
}, skinsRepo, accountApi, wd
|
|
||||||
}
|
|
220
worker/worker.go
220
worker/worker.go
@ -1,220 +0,0 @@
|
|||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/assembla/cony"
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
"github.com/streadway/amqp"
|
|
||||||
|
|
||||||
"elyby/minecraft-skinsystem/db"
|
|
||||||
"elyby/minecraft-skinsystem/interfaces"
|
|
||||||
"elyby/minecraft-skinsystem/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Services struct {
|
|
||||||
AmqpClient *cony.Client
|
|
||||||
SkinsRepo interfaces.SkinsRepository
|
|
||||||
AccountsAPI interfaces.AccountsAPI
|
|
||||||
Logger wd.Watchdog
|
|
||||||
}
|
|
||||||
|
|
||||||
type UsernameChanged struct {
|
|
||||||
AccountId int `json:"accountId"`
|
|
||||||
OldUsername string `json:"oldUsername"`
|
|
||||||
NewUsername string `json:"newUsername"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkinChanged struct {
|
|
||||||
AccountId int `json:"userId"`
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
SkinId int `json:"skinId"`
|
|
||||||
OldSkinId int `json:"oldSkinId"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Is1_8 bool `json:"is1_8"`
|
|
||||||
IsSlim bool `json:"isSlim"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
MojangTextures string `json:"mojangTextures"`
|
|
||||||
MojangSignature string `json:"mojangSignature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const exchangeName string = "events"
|
|
||||||
const queueName string = "skinsystem-accounts-events"
|
|
||||||
|
|
||||||
func (service *Services) Run() error {
|
|
||||||
clientErrs, consumerErrs, deliveryChannel := setupClient(service.AmqpClient)
|
|
||||||
shouldReturnError := true
|
|
||||||
|
|
||||||
for service.AmqpClient.Loop() {
|
|
||||||
select {
|
|
||||||
case msg := <-deliveryChannel:
|
|
||||||
shouldReturnError = false
|
|
||||||
service.HandleDelivery(&msg)
|
|
||||||
case err := <-consumerErrs:
|
|
||||||
if shouldReturnError {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Logger.Error("Consume error: :err", wd.ErrParam(err))
|
|
||||||
case err := <-clientErrs:
|
|
||||||
if shouldReturnError {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Logger.Error("Client error: :err", wd.ErrParam(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Services) HandleDelivery(delivery *amqp.Delivery) {
|
|
||||||
service.Logger.Debug("Incoming message with routing key " + delivery.RoutingKey)
|
|
||||||
var result bool = true
|
|
||||||
switch delivery.RoutingKey {
|
|
||||||
case "accounts.username-changed":
|
|
||||||
var event *UsernameChanged
|
|
||||||
json.Unmarshal(delivery.Body, &event)
|
|
||||||
result = service.HandleChangeUsername(event)
|
|
||||||
case "accounts.skin-changed":
|
|
||||||
var event *SkinChanged
|
|
||||||
json.Unmarshal(delivery.Body, &event)
|
|
||||||
result = service.HandleSkinChanged(event)
|
|
||||||
default:
|
|
||||||
service.Logger.Info("Unknown delivery with routing key " + delivery.RoutingKey)
|
|
||||||
delivery.Ack(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if result {
|
|
||||||
delivery.Ack(false)
|
|
||||||
} else {
|
|
||||||
delivery.Reject(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Services) HandleChangeUsername(event *UsernameChanged) bool {
|
|
||||||
service.Logger.IncCounter("worker.change_username", 1)
|
|
||||||
if event.OldUsername == "" {
|
|
||||||
service.Logger.IncCounter("worker.change_username_empty_old_username", 1)
|
|
||||||
record := &model.Skin{
|
|
||||||
UserId: event.AccountId,
|
|
||||||
Username: event.NewUsername,
|
|
||||||
}
|
|
||||||
|
|
||||||
service.SkinsRepo.Save(record)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err := service.SkinsRepo.FindByUserId(event.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId))
|
|
||||||
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
|
||||||
service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Logger.IncCounter("worker.change_username_id_not_found", 1)
|
|
||||||
record = &model.Skin{
|
|
||||||
UserId: event.AccountId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Username = event.NewUsername
|
|
||||||
service.SkinsRepo.Save(record)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: возможно стоит добавить проверку на совпадение id аккаунтов
|
|
||||||
func (service *Services) HandleSkinChanged(event *SkinChanged) bool {
|
|
||||||
service.Logger.IncCounter("worker.skin_changed", 1)
|
|
||||||
var record *model.Skin
|
|
||||||
record, err := service.SkinsRepo.FindByUserId(event.AccountId)
|
|
||||||
if err != nil {
|
|
||||||
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
|
||||||
service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Logger.IncCounter("worker.skin_changed_id_not_found", 1)
|
|
||||||
service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId))
|
|
||||||
response, err := service.AccountsAPI.AccountInfo("id", strconv.Itoa(event.AccountId))
|
|
||||||
if err != nil {
|
|
||||||
service.Logger.IncCounter("worker.skin_changed_id_not_restored", 1)
|
|
||||||
service.Logger.Error(
|
|
||||||
"Cannot restore user info for :accountId: :err",
|
|
||||||
wd.IntParam("accountId", event.AccountId),
|
|
||||||
wd.ErrParam(err),
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Logger.IncCounter("worker.skin_changed_id_restored", 1)
|
|
||||||
service.Logger.Info("User info successfully restored.")
|
|
||||||
|
|
||||||
record = &model.Skin{
|
|
||||||
UserId: response.Id,
|
|
||||||
Username: response.Username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Uuid = event.Uuid
|
|
||||||
record.SkinId = event.SkinId
|
|
||||||
record.Hash = event.Hash
|
|
||||||
record.Is1_8 = event.Is1_8
|
|
||||||
record.IsSlim = event.IsSlim
|
|
||||||
record.Url = event.Url
|
|
||||||
record.MojangTextures = event.MojangTextures
|
|
||||||
record.MojangSignature = event.MojangSignature
|
|
||||||
|
|
||||||
service.SkinsRepo.Save(record)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupClient(client *cony.Client) (<-chan error, <-chan error, <-chan amqp.Delivery ) {
|
|
||||||
exchange := cony.Exchange{
|
|
||||||
Name: exchangeName,
|
|
||||||
Kind: "topic",
|
|
||||||
Durable: true,
|
|
||||||
AutoDelete: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
queue := &cony.Queue{
|
|
||||||
Name: queueName,
|
|
||||||
Durable: true,
|
|
||||||
AutoDelete: false,
|
|
||||||
Exclusive: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
usernameEventBinding := cony.Binding{
|
|
||||||
Exchange: exchange,
|
|
||||||
Queue: queue,
|
|
||||||
Key: "accounts.username-changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
skinEventBinding := cony.Binding{
|
|
||||||
Exchange: exchange,
|
|
||||||
Queue: queue,
|
|
||||||
Key: "accounts.skin-changed",
|
|
||||||
}
|
|
||||||
|
|
||||||
declarations := []cony.Declaration{
|
|
||||||
cony.DeclareExchange(exchange),
|
|
||||||
cony.DeclareQueue(queue),
|
|
||||||
cony.DeclareBinding(usernameEventBinding),
|
|
||||||
cony.DeclareBinding(skinEventBinding),
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Declare(declarations)
|
|
||||||
|
|
||||||
consumer := cony.NewConsumer(queue,
|
|
||||||
cony.Qos(10),
|
|
||||||
cony.AutoTag(),
|
|
||||||
)
|
|
||||||
client.Consume(consumer)
|
|
||||||
|
|
||||||
return client.Errors(), consumer.Errors(), consumer.Deliveries()
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user