feat:init project

This commit is contained in:
gelik 2023-05-16 04:12:07 +03:00
parent 8e220262a8
commit 9526f3e32f
102 changed files with 7492 additions and 85 deletions

8
.dockerignore Normal file

@ -0,0 +1,8 @@
.mockery.yaml
.golangci.yaml
.gitlab-ci.yaml
.gitingore
.Makefile
.README.md
deployments
tests

42
.env.test Normal file

@ -0,0 +1,42 @@
# HTTP settings
HTTP_HOST=0.0.0.0
HTTP_PORT=8080
# MONGO settings
MONGO_HOST=mongo
MONGO_PORT=27017
MONGO_USER=test
MONGO_PASSWORD=test
MONGO_AUTH=admin
MONGO_DB_NAME=admin
# GOOGLE settings
GOOGLE_REDIRECT_URL=http://localhost:8080/google/callback
GOOGLE_CLIENT_ID=test.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=test-0VOAAx87vr7epBTG
GOOGLE_OAUTH_HOST=https://www.googleapis.com/oauth2/v3
# VK settings
VK_REDIRECT_URL=http://localhost:8080/vk/callback
VK_CLIENT_ID=51394112
VK_CLIENT_SECRET=test_ursJCpEY
# Amocrm settings
AMOCRM_CLIENT_ID=d677e851-03e0-467e-a5de-2ebcf6d47529
AMOCRM_CLIENT_SECRET=fOzWUMyF6fGmAkgLULN0H4wFUClVTsaVTPOmVazit4cQzuDM1JhGr3MP6IZIcKmj
AMOCRM_REDIRECT_URL=https://oauth.pena.digital/amocrm/callback
AMOCRM_OAUTH_HOST=https://www.amocrm.ru/oauth
AMOCRM_USER_INFO_URL=http://localhost:8000/api/v4/account
AMOCRM_ACCESS_TOKEN_URL=http://localhost:8000/oauth2/access_token
# Auth Microservice settings
AUTH_MICROSERVICE_GROUP=group
AUTH_MICROSERVICE_PRIVATE_SIGN_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs\n-----END PRIVATE KEY-----"
AUTH_MICROSERVICE_EXHANGE_URL=http://localhost:8000/exchange
AUTH_MICROSERVICE_REGISTER_URL=http://localhost:8000/register
AUTH_MICROSERVICE_USER_URL=http://localhost:8000/user
# JWT settings
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyt4XuLovUY7i12K2PIMbQZOKn+wFFKUvxvKQDel049/+VMpHMx1FLolUKuyGp9zi6gOwjHsBPgc9oqr/eaXGQSh7Ult7i9f+Ht563Y0er5UU9Zc5ZPSxf9O75KYD48ruGkqiFoncDqPENK4dtUa7w0OqlN4bwVBbmIsP8B3EDC5Dof+vtiNTSHSXPx+zifKeZGyknp+nyOHVrRDhPjOhzQzCom0MSZA/sJYmps8QZgiPA0k4Z6jTupDymPOIwYeD2C57zSxnAv0AfC3/pZYJbZYH/0TszRzmy052DME3zMnhMK0ikdN4nzYqU0dkkA5kb5GtKDymspHIJ9eWbUuwgtg8Rq/LrVBj1I3UFgs0ibio40k6gqinLKslc5Y1I5mro7J3OSEP5eO/XeDLOLlOJjEqkrx4fviI1cL3m5L6QV905xmcoNZG1+RmOg7D7cZQUf27TXqM381jkbNdktm1JLTcMScxuo3vaRftnIVw70V8P8sIkaKY8S8HU1sQgE2LB9t04oog5u59htx2FHv4B13NEm8tt8Tv1PexpB4UVh7PIualF6SxdFBrKbraYej72wgjXVPQ0eGXtGGD57j8DUEzk7DK2OvIWhehlVqtiRnFdAvdBj2ynHT2/5FJ/Zpd4n5dKGJcQvy1U1qWMs+8M7AHfWyt2+nZ04s48+bK3yMCAwEAAQ==\n-----END PUBLIC KEY-----"
JWT_ISSUER="issuer"
JWT_AUDIENCE="audience"

8
.gitignore vendored Normal file

@ -0,0 +1,8 @@
/proto
# Dependency directories (remove the comment below to include it)
# vendor/
.idea/
.vscode
.env

99
.gitlab-ci.yml Normal file

@ -0,0 +1,99 @@
stages:
- lint
- test
- clean
- build
- deploy
lint:
image: golangci/golangci-lint:v1.52-alpine
stage: lint
before_script:
- go install github.com/vektra/mockery/v2@v2.26.0
script:
- go generate ./...
- golangci-lint version
- golangci-lint run ./...
test:
image: golang:1.20.3-alpine
stage: test
coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/
script:
- CGO_ENABLED=0 go test ./... -coverprofile=coverage.out
- go tool cover -html=coverage.out -o coverage.html
- go tool cover -func coverage.out
artifacts:
expire_in: "3 days"
paths:
- coverage.html
clean-old:
stage: clean
image:
name: docker/compose:1.28.0
entrypoint: [""]
allow_failure: true
variables:
PRODUCTION_BRANCH: main
STAGING_BRANCH: "staging"
DEPLOY_TO: "staging"
rules:
- if: $CI_COMMIT_BRANCH == $PRODUCTION_BRANCH || $CI_COMMIT_BRANCH == $STAGING_BRANCH
when: on_success
before_script:
- echo DEPLOY_TO = $DEPLOY_TO
script:
- docker-compose -f deployments/$DEPLOY_TO/docker-compose.yaml down --volumes --rmi local
build-app:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main"
rules:
- if: $CI_COMMIT_BRANCH == $PRODUCTION_BRANCH || $CI_COMMIT_BRANCH == $STAGING_BRANCH
when: on_success
before_script:
- echo PRODUCTION_BRANCH = $PRODUCTION_BRANCH
- echo STAGING_BRANCH = $STAGING_BRANCH
- echo CI_REGISTRY = $CI_REGISTRY
- echo CI_REGISTRY_USER = $CI_REGISTRY_USER
- echo CI_PROJECT_DIR = $CI_PROJECT_DIR
- echo CI_REGISTRY_IMAGE = $CI_REGISTRY_IMAGE
- echo CI_COMMIT_REF_SLUG = $CI_COMMIT_REF_SLUG
- echo DOCKER_BUILD_PATH = $DOCKER_BUILD_PATH
- echo CI_PIPELINE_ID = $CI_PIPELINE_ID
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- |
/kaniko/executor --context $CI_PROJECT_DIR \
--cache=true --cache-repo=$CI_REGISTRY_IMAGE \
--dockerfile $CI_PROJECT_DIR/$DOCKER_BUILD_PATH --target production \
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
deploy-to-staging:
stage: deploy
image:
name: docker/compose:1.28.0
entrypoint: [""]
variables:
DEPLOY_TO: "staging"
BRANCH: "staging"
rules:
- if: $CI_COMMIT_BRANCH == $BRANCH
before_script:
- echo CI_PROJECT_NAME = $CI_PROJECT_NAME
- echo CI_REGISTRY = $CI_REGISTRY
- echo REGISTRY_USER = $REGISTRY_USER
- echo REGISTRY_TOKEN = $REGISTRY_TOKEN
- echo DEPLOY_TO = $DEPLOY_TO
- echo BRANCH = $BRANCH
script:
- docker login -u $REGISTRY_USER -p $REGISTRY_TOKEN $CI_REGISTRY
- docker-compose -f deployments/$DEPLOY_TO/docker-compose.yaml up -d

162
.golangci.yaml Normal file

@ -0,0 +1,162 @@
run:
timeout: 5m
skip-files:
- \.pb\.go$
- \.pb\.validate\.go$
- \.pb\.gw\.go$
skip-dirs:
- mocks
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- depguard
- dogsled
- dupword
- durationcheck
- errcheck
- errchkjson
- exportloopref
- goconst
- gocritic
- godot
- gofmt
- gci
- goprintffuncname
- gosec
- gosimple
- govet
- importas
- ineffassign
- misspell
- nakedret
- nilerr
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- revive
- rowserrcheck
- staticcheck
- stylecheck
- thelper
- typecheck
- unconvert
- unparam
- unused
- usestdlibvars
- whitespace
linters-settings:
errcheck:
exclude-functions:
- (io.Closer).Close
govet:
check-shadowing: true
gci:
custom-order: false
section-separators:
- newLine
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled.
- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled.
importas:
no-unaliased: true
alias:
# Foundation libraries
- pkg: git.sbercloud.tech/products/paas/shared/foundation/management-server
alias: mgmtserver
maligned:
suggest-new: true
goconst:
min-len: 2
min-occurrences: 2
lll:
line-length: 140
revive:
rules:
# The following rules are recommended https://github.com/mgechev/revive#recommended-configuration
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
# - name: exported
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
# - name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id
#
# Rules in addition to the recommended configuration above.
#
- name: bool-literal-in-expr
- name: constant-logical-expr
gosec:
excludes:
- G307 # Deferring unsafe method "Close" on type "\*os.File"
- G108 # Profiling endpoint is automatically exposed on /debug/pprof
gocritic:
enabled-tags:
- diagnostic
- experimental
- performance
disabled-checks:
- appendAssign
- dupImport # https://github.com/go-critic/go-critic/issues/845
- evalOrder
- ifElseChain
- octalLiteral
- regexpSimplify
- sloppyReassign
- truncateCmp
- typeDefFirst
- unnamedResult
- unnecessaryDefer
- whyNoLint
- wrapperFunc
- rangeValCopy
- hugeParam
issues:
exclude-rules:
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck
- text: "should have a package comment, unless it's in another file for this package"
linters:
- golint
- text: "should have comment or be unexported"
linters:
- golint
- path: _test\.go
linters:
- gosec
- dupl
exclude-use-default: false
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
print-linter-name: true

6
.mockery.yaml Normal file

@ -0,0 +1,6 @@
exported: True
inpackage: False
keeptree: True
case: underscore
with-expecter: True
inpackage-suffix: True

75
Dockerfile Normal file

@ -0,0 +1,75 @@
# BUILD
FROM golang:1.20.3-alpine AS build
# Update depences
RUN apk update && apk add --no-cache curl
# Create build directory
RUN mkdir /app/bin -p
RUN mkdir /bin/golang-migrate -p
# Download migrate app
RUN GOLANG_MIGRATE_VERSION=v4.15.1 && \
curl -L https://github.com/golang-migrate/migrate/releases/download/${GOLANG_MIGRATE_VERSION}/migrate.linux-amd64.tar.gz |\
tar xvz migrate -C /bin/golang-migrate
# Download health check utility
RUN GRPC_HEALTH_PROBE_VERSION=v0.4.6 && \
wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
chmod +x /bin/grpc_health_probe
# Download debugger
# Set home directory
WORKDIR /app
# Copy go.mod
ADD go.mod go.sum /app/
ADD dlv /app/
# Download go depences
RUN go mod download
# Copy all local files
ADD . /app
# Build app
RUN GOOS=linux go build -o bin ./...
# TEST
FROM alpine:latest AS test
# Install packages
RUN apk --no-cache add ca-certificates
ENV GO111MODULE=off
# Create home directory
WORKDIR /app
# Copy build file
COPY --from=build /app/bin/app ./app
COPY --from=build /app/dlv ./
# CMD
CMD [ "./app" ]
# MIGRATION
FROM alpine:latest AS migration
# Install packages
RUN apk --no-cache add ca-certificates
# Create home directory
WORKDIR /app
# Copy migration dir
COPY --from=build /app/migrations/tests ./migrations
# Install migrate tool
COPY --from=build /bin/golang-migrate /usr/local/bin
# PRODUCTION
FROM alpine:latest AS production
# Install packages
RUN apk --no-cache add ca-certificates
# Create home directory
WORKDIR /app
# Copy build file
COPY --from=build /app/bin/app ./app
# Copy grpc health probe dir
COPY --from=build /bin/grpc_health_probe /bin/grpc_health_probe
# Install migrate tool
COPY --from=build /bin/golang-migrate /usr/local/bin
# CMD
CMD ["./app"]

36
Makefile Normal file

@ -0,0 +1,36 @@
SERVICE_NAME = pena-social-auth
help: ## show this help
@echo 'usage: make [target] ...'
@echo ''
@echo 'targets:'
@egrep '^(.+)\:\ .*##\ (.+)' ${MAKEFILE_LIST} | sed 's/:.*##/#/' | column -t -c 2 -s '#'
install: ## install all go dependencies
go install \
github.com/vektra/mockery/v2@v2.26.0
test: ## run all layers tests
@make test.unit
@make test.integration
test.unit: ## run unit tests
go test ./...
test.integration: ## run integration tests
@make test.integration.up
@make test.integration.start
@make test.integration.down
test.integration.up: ## build integration test environment
docker-compose -f deployments/test/docker-compose.yaml --env-file ./.env.test up -d
test.integration.start: ## run integration test
go test -tags integration ./tests/integration/...
test.integration.down: ## shutting down integration environment
docker-compose -f deployments/test/docker-compose.yaml --env-file ./.env.test down --volumes --rmi local
run: ## run app
go run ./cmd/app/main.go

107
README.md

@ -1,92 +1,29 @@
# customer
Сервис customer
| Branch | Pipeline | Code coverage |
| ------------- |:-----------------:| --------------:|
| main | [![pipeline status](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/main/pipeline.svg)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) | [![coverage report](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/main/coverage.svg?job=test)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) |
| staging | [![pipeline status](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/staging/pipeline.svg)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) | [![coverage report](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/staging/coverage.svg?job=test)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) |
| dev | [![pipeline status](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/dev/pipeline.svg)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) | [![coverage report](https://penahub.gitlab.yandexcloud.net/pena-services/customer/badges/dev/coverage.svg?job=test)](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/pipelines) |
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
## Переменные окружения приложения
```
cd existing_repo
git remote add origin https://penahub.gitlab.yandexcloud.net/pena-services/customer.git
git branch -M main
git push -uf origin main
HTTP_HOST - хост приложения
HTTP_PORT - порт приложения
MONGO_HOST - хост MongoDB
MONGO_PORT - порт MongoDB
MONGO_USER - пользователь MongoDB
MONGO_DB_NAME - название базы данных для подключения
MONGO_PASSWORD - пароль пользователя MongoDB
MONGO_AUTH - имя базы данных Mongo, по которой будет производится авторизация
AUTH_MICROSERVICE_USER_URL - ссылка на получение пользователя микросервиса авторизации
JWT_PUBLIC_KEY - публичный ключ для верификации jwt токена
JWT_ISSUER - издатель токена
JWT_AUDIENCE - аудитория, которая может верифицировать токен
```
## Integrate with your tools
- [ ] [Set up project integrations](https://penahub.gitlab.yandexcloud.net/pena-services/customer/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

43
cmd/app/main.go Normal file

@ -0,0 +1,43 @@
package main
import (
"context"
"fmt"
"os/signal"
"path"
"runtime"
"strconv"
"syscall"
formatter "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/app"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
logger := logrus.New()
defer cancel()
logger.SetReportCaller(true)
logger.SetFormatter(&formatter.Formatter{
TimestampFormat: "02-01-2006 15:04:05",
HideKeys: true,
NoColors: false,
NoFieldsSpace: true,
CustomCallerFormatter: func(frame *runtime.Frame) string {
return fmt.Sprintf(" (%s:%s)", path.Base(frame.File), strconv.Itoa(frame.Line))
},
})
config, err := initialize.Configuration(".env.test")
if err != nil {
logger.Fatalf("failed to init config: %v", err)
}
if err := app.Run(ctx, config, logger); err != nil {
logger.Fatalf("failed to run app: %v", err)
}
}

@ -0,0 +1,53 @@
version: "3.3"
services:
pena-social-auth-service:
container_name: pena-social-auth-service
restart: unless-stopped
tty: true
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
environment:
- ENVIRONMENT=staging
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8000
- MONGO_HOST=10.6.0.11
- MONGO_PORT=27017
- MONGO_USER=$MONGO_USER
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_DB_NAME=socialAuth
- MONGO_AUTH=socialAuth
- JWT_PUBLIC_KEY=$JWT_PUBLIC_KEY
- JWT_ISSUER=pena-auth-service
- JWT_AUDIENCE=pena
- AMOCRM_REDIRECT_URL=https://oauth.pena.digital/amocrm/callback
- AMOCRM_OAUTH_HOST=https://www.amocrm.ru/oauth
- AMOCRM_USER_INFO_URL=https://penadigital.amocrm.ru/api/v4/account
- AMOCRM_ACCESS_TOKEN_URL=https://penadigital.amocrm.ru/oauth2/access_token
- AMOCRM_CLIENT_ID=$AMOCRM_CLIENT_ID
- AMOCRM_CLIENT_SECRET=$AMOCRM_CLIENT_SECRET
- AUTH_MICROSERVICE_GROUP=$AUTH_MICROSERVICE_GROUP
- AUTH_MICROSERVICE_PRIVATE_SIGN_KEY=$AUTH_MICROSERVICE_PRIVATE_SIGN_KEY
- AUTH_MICROSERVICE_EXHANGE_URL=http://pena-auth-service:8080/auth/exchange
- AUTH_MICROSERVICE_REGISTER_URL=http://pena-auth-service:8080/auth/register
- AUTH_MICROSERVICE_USER_URL=http://pena-auth-service:8080/user
- GOOGLE_REDIRECT_URL=http://localhost:8080/google/callback
- GOOGLE_CLIENT_ID=test.apps.googleusercontent.com
- GOOGLE_CLIENT_SECRET=test-0VOAAx87vr7epBTG
- GOOGLE_OAUTH_HOST=https://www.googleapis.com/oauth2/v3
- VK_REDIRECT_URL=http://localhost:8080/vk/callback
- VK_CLIENT_ID=51394112
- VK_CLIENT_SECRET=test_ursJCpEY
expose:
- 8000
networks:
- marketplace_penahub_frontend
networks:
marketplace_penahub_frontend:
external: true

@ -0,0 +1,61 @@
version: "3"
services:
app:
build:
context: ../../.
dockerfile: Dockerfile
target: test
env_file:
- ../../.env.test
environment:
- AUTH_MICROSERVICE_EXHANGE_URL=http://mock:8080/exchange
- AUTH_MICROSERVICE_REGISTER_URL=http://mock:8080/register
- AUTH_MICROSERVICE_USER_URL=http://mock:8080/user
- AMOCRM_USER_INFO_URL=http://mock:8080/api/v4/account
- AMOCRM_ACCESS_TOKEN_URL=http://mock:8080/oauth2/access_token
- MONGO_HOST=mongo
ports:
- 8080:8080
depends_on:
- migration
networks:
- integration_test
migration:
build:
context: ../../.
dockerfile: Dockerfile
target: migration
command:
[
"sh",
"-c",
'migrate -source file://migrations -database "mongodb://$MONGO_USER:$MONGO_PASSWORD@$MONGO_HOST:$MONGO_PORT/$MONGO_AUTH?authSource=$MONGO_AUTH" up',
]
depends_on:
- mongo
networks:
- integration_test
mongo:
image: 'mongo:6.0.3'
environment:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- '27017:27017'
networks:
- integration_test
mock:
image: 'wiremock/wiremock:2.35.0'
ports:
- 8000:8080
networks:
- integration_test
depends_on:
- app
networks:
integration_test:

56
go.mod Normal file

@ -0,0 +1,56 @@
module penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth
go 1.20
require (
github.com/SevereCloud/vksdk/v2 v2.16.0
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/echo/v4 v4.10.2
github.com/sethvargo/go-envconfig v0.9.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.2
github.com/walkerus/go-wiremock v1.5.0
go.mongodb.org/mongo-driver v1.11.4
golang.org/x/oauth2 v0.6.0
)
require (
cloud.google.com/go/compute/metadata v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

152
go.sum Normal file

@ -0,0 +1,152 @@
cloud.google.com/go/compute/metadata v0.2.0 h1:nBbNSZyDpkNlo3DepaaLKVuO7ClyifSAmNloSCZrHnQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/SevereCloud/vksdk/v2 v2.16.0 h1:DQ90qqwY/yF1X/SWZQs1kQ/Ik+tphK82d+S6Rch46wQ=
github.com/SevereCloud/vksdk/v2 v2.16.0/go.mod h1:VN6BH9nFUXcP7Uf0uX74Aht2DQ7+139aG3/Og+jia4w=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE=
github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/walkerus/go-wiremock v1.5.0 h1:ipaYzaZnnOJRQS4wNFqz4YFphC/sM9GM+EiLEzv3KLc=
github.com/walkerus/go-wiremock v1.5.0/go.mod h1:gMzQpReT5mG5T/PaW8pSFiPhazrcHb1mnf6JHdKwY5w=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

75
internal/app/app.go Normal file

@ -0,0 +1,75 @@
package app
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/server"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/closer"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/mongo"
)
const (
shutdownTimeout = 5 * time.Second
)
func Run(ctx context.Context, config *models.Config, logger *logrus.Logger) error {
mongoDB, err := mongo.Connect(ctx, &mongo.ConnectDeps{
Configuration: &config.Database,
Timeout: 10 * time.Second,
})
if err != nil {
return fmt.Errorf("failed connection to db: %w", err)
}
clients := initialize.NewClients(&initialize.ClientsDeps{
Logger: logger,
GoogleURL: &config.Service.Google.URL,
AmocrmURL: &config.Service.Amocrm.URL,
AuthURL: &config.Service.AuthMicroservice.URL,
AmocrmOAuthConfiguration: &config.Service.Amocrm.OAuthConfig,
})
repositories := initialize.NewRepositories(&initialize.RepositoriesDeps{
Logger: logger,
MongoDB: mongoDB,
})
services := initialize.NewServices(&initialize.ServicesDeps{
Logger: logger,
Config: &config.Service,
Repositories: repositories,
Clients: clients,
})
controllers := initialize.NewControllers(&initialize.ControllersDeps{
Logger: logger,
Services: services,
})
httpServer := server.New(logger).Register(controllers)
closer := closer.New(logger)
go httpServer.Run(&config.HTTP)
closer.Add(mongoDB.Client().Disconnect)
closer.Add(httpServer.Stop)
<-ctx.Done()
logger.Infoln("shutting down app gracefully")
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := closer.Close(shutdownCtx); err != nil {
return fmt.Errorf("closer: %w", err)
}
return nil
}

43
internal/client/amocrm.go Normal file

@ -0,0 +1,43 @@
package client
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/client"
)
type AmocrmClientDeps struct {
Logger *logrus.Logger
URLs *models.AmocrmURL
}
type AmocrmClient struct {
logger *logrus.Logger
urls *models.AmocrmURL
}
func NewAmocrmClient(deps *AmocrmClientDeps) *AmocrmClient {
return &AmocrmClient{
logger: deps.Logger,
urls: deps.URLs,
}
}
func (receiver *AmocrmClient) GetUserInformation(ctx context.Context, accessToken string) (*models.AmocrmUserInformation, error) {
response, err := client.Get[models.AmocrmUserInformation, any](ctx, &client.RequestSettings{
URL: receiver.urls.UserInfo,
Headers: map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", accessToken),
"Content-Type": "application/json",
},
})
if err != nil {
receiver.logger.Errorf("failed to get user information on <GetUserInformation> of <AmocrmClient>: %v", err)
return nil, err
}
return response.Body, nil
}

90
internal/client/auth.go Normal file

@ -0,0 +1,90 @@
package client
import (
"context"
"net/url"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/client"
)
type AuthClientDeps struct {
Logger *logrus.Logger
URLs *models.AuthMicroServiceURL
}
type AuthClient struct {
logger *logrus.Logger
urls *models.AuthMicroServiceURL
}
func NewAuthClient(deps *AuthClientDeps) *AuthClient {
return &AuthClient{
logger: deps.Logger,
urls: deps.URLs,
}
}
func (receiver *AuthClient) GetUser(ctx context.Context, userID string) (*models.AuthUser, error) {
userURL, err := url.JoinPath(receiver.urls.User, userID)
if err != nil {
return nil, errors.ErrInvalidReturnValue
}
response, err := client.Get[models.AuthUser, models.FastifyError](ctx, &client.RequestSettings{
URL: userURL,
Headers: map[string]string{"Content-Type": "application/json"},
})
if err != nil {
receiver.logger.Errorf("failed to get user on <GetUser> of <AuthClient>: %v", err)
return nil, err
}
if response.Error != nil {
receiver.logger.Errorf("failed request on <GetUser> of <AuthClient>: %s", response.Error.Message)
return nil, utils.DetermineClientErrorResponse(response.StatusCode)
}
return response.Body, nil
}
func (receiver *AuthClient) Register(ctx context.Context, request *models.RegisterRequest) (*models.Tokens, error) {
response, err := client.Post[models.Tokens, models.FastifyError](ctx, &client.RequestSettings{
URL: receiver.urls.Register,
Headers: map[string]string{"Content-Type": "application/json"},
Body: request,
})
if err != nil {
receiver.logger.Errorf("failed to register user on <Register> of <AuthClient>: %v", err)
return nil, err
}
if response.Error != nil {
receiver.logger.Errorf("failed request on <Register> of <AuthClient>: %s", response.Error.Message)
return nil, utils.DetermineClientErrorResponse(response.StatusCode)
}
return response.Body, nil
}
func (receiver *AuthClient) Exchange(ctx context.Context, userID, signature string) (*models.Tokens, error) {
response, err := client.Post[models.Tokens, models.FastifyError](ctx, &client.RequestSettings{
URL: receiver.urls.Exchange,
Headers: map[string]string{"Content-Type": "application/json"},
Body: models.ExchangeRequest{
UserID: userID,
Signature: signature,
},
})
if err != nil {
receiver.logger.Errorf("failed to exchange code on <Exchange> of <AuthClient>: %v", err)
return nil, err
}
if response.Error != nil {
receiver.logger.Errorf("failed request on <Exchange> of <AuthClient>: %s", response.Error.Message)
return nil, utils.DetermineClientErrorResponse(response.StatusCode)
}
return response.Body, nil
}

43
internal/client/google.go Normal file

@ -0,0 +1,43 @@
package client
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/client"
)
type GoogleClientDeps struct {
Logger *logrus.Logger
URLs *models.GoogleURL
}
type GoogleClient struct {
logger *logrus.Logger
urls *models.GoogleURL
}
func NewGoogleClient(deps *GoogleClientDeps) *GoogleClient {
return &GoogleClient{
logger: deps.Logger,
urls: deps.URLs,
}
}
func (receiver *GoogleClient) GetUserInformation(ctx context.Context, accessToken string) (*models.GoogleUserInformation, error) {
response, err := client.Get[models.GoogleUserInformation, any](ctx, &client.RequestSettings{
URL: receiver.urls.UserInfo,
Headers: map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", accessToken),
"Content-Type": "application/json",
},
})
if err != nil {
receiver.logger.Errorf("failed to get user information on <GetUserInformation> of <GoogleClient>: %v", err)
return nil, err
}
return response.Body, nil
}

68
internal/client/oauth.go Normal file

@ -0,0 +1,68 @@
package client
import (
"context"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/client"
)
type OAuthClientDeps struct {
Logger *logrus.Logger
Config *oauth2.Config
}
type OAuthClient struct {
logger *logrus.Logger
config *oauth2.Config
}
func NewOAuthClient(deps *OAuthClientDeps) *OAuthClient {
return &OAuthClient{
logger: deps.Logger,
config: deps.Config,
}
}
func (receiver *OAuthClient) Exchange(ctx context.Context, code string, options ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
if receiver.config.Endpoint.AuthStyle < 4 {
return receiver.config.Exchange(ctx, code, options...)
}
if receiver.config.Endpoint.AuthStyle == models.BodyAuthStyle {
return receiver.bodyExchange(ctx, code)
}
return nil, nil
}
func (receiver *OAuthClient) AuthCodeURL(state string, options ...oauth2.AuthCodeOption) string {
return receiver.config.AuthCodeURL(state, options...)
}
func (receiver *OAuthClient) bodyExchange(ctx context.Context, code string) (*oauth2.Token, error) {
response, err := client.Post[oauth2.Token, any](ctx, &client.RequestSettings{
URL: receiver.config.Endpoint.TokenURL,
Headers: map[string]string{"Content-Type": "application/json"},
Body: map[string]string{
"grant_type": "authorization_code",
"redirect_uri": receiver.config.RedirectURL,
"client_id": receiver.config.ClientID,
"client_secret": receiver.config.ClientSecret,
"code": code,
},
})
if err != nil {
receiver.logger.Errorf("failed to exchange code on <bodyExchange> of <OAuthClient>: %v", err)
return nil, err
}
if response.Error != nil {
receiver.logger.Errorf("failed request on <bodyExchange> of <OAuthClient>: %s", receiver.config.Endpoint.TokenURL)
return nil, utils.DetermineClientErrorResponse(response.StatusCode)
}
return response.Body, nil
}

34
internal/client/vk.go Normal file

@ -0,0 +1,34 @@
package client
import (
"github.com/SevereCloud/vksdk/v2/api"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
type VKClient struct {
logger *logrus.Logger
}
func NewVKClient(logger *logrus.Logger) *VKClient {
return &VKClient{logger: logger}
}
func (receiver *VKClient) GetUserInformation(token *oauth2.Token) (*models.VKUserInformation, error) {
vk := api.NewVK(token.AccessToken)
userInformations := []models.VKUserInformation{}
if err := vk.RequestUnmarshal("users.get", &userInformations, api.Params{
"fields": "id,photo_400_orig,sex,domain,screen_name,bdate,photo_id,followers_count,home_town,timezone,mobile_phone",
}); err != nil {
receiver.logger.Errorln("request error: ", err.Error())
return nil, err
}
currentUserInformation := userInformations[0]
currentUserInformation.Email = token.Extra("email").(string)
return &currentUserInformation, nil
}

@ -0,0 +1,99 @@
package amocrm
import (
"context"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
)
//go:generate mockery --name amocrmService
type amocrmService interface {
Auth(ctx context.Context, code string) (*models.Tokens, error)
Link(ctx context.Context, code, accessToken string) (bool, error)
}
//go:generate mockery --name oauthService
type oauthService interface {
GenerateAuthURL() string
GenerateLinkURL(accessToken string) string
ValidateState(state string) bool
}
type Deps struct {
Logger *logrus.Logger
AmocrmService amocrmService
OAuthService oauthService
}
type Controller struct {
logger *logrus.Logger
amocrmService amocrmService
oauthService oauthService
}
func New(deps *Deps) *Controller {
return &Controller{
logger: deps.Logger,
oauthService: deps.OAuthService,
amocrmService: deps.AmocrmService,
}
}
func (receiver *Controller) RedirectAuthURL(ctx echo.Context) error {
return ctx.Redirect(http.StatusTemporaryRedirect, receiver.oauthService.GenerateAuthURL())
}
func (receiver *Controller) GenerateAuthURL(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, models.GenerateURLResponse{
URL: receiver.oauthService.GenerateAuthURL(),
})
}
func (receiver *Controller) RedirectLinkAccountURL(ctx echo.Context) error {
accessToken := ctx.QueryParam("accessToken")
return ctx.Redirect(http.StatusTemporaryRedirect, receiver.oauthService.GenerateLinkURL(accessToken))
}
func (receiver *Controller) GenerateLinkAccountURL(ctx echo.Context) error {
accessToken := ctx.QueryParam("accessToken")
return ctx.JSON(http.StatusOK, models.GenerateURLResponse{
URL: receiver.oauthService.GenerateLinkURL(accessToken),
})
}
func (receiver *Controller) Callback(ctx echo.Context) error {
callbackState := ctx.QueryParam("state")
callbackCode := ctx.QueryParam("code")
callbackAccessToken := ctx.QueryParam("accessToken")
if !receiver.oauthService.ValidateState(callbackState) {
receiver.logger.Errorln("state is not valid on <Callback> of <AmocrmController>")
return utils.DetermineEchoErrorResponse(ctx, errors.ErrInvalidArgs, "state is not valid")
}
if callbackAccessToken != "" {
tokens, err := receiver.amocrmService.Link(ctx.Request().Context(), callbackCode, callbackAccessToken)
if err != nil {
receiver.logger.Errorf("failed to link amocrm account on <Callback> of <AmocrmAuthController>: %v", err)
return utils.DetermineEchoErrorResponse(ctx, err, fmt.Sprintf("failed to link amocrm account: %v", err))
}
return ctx.JSON(http.StatusOK, tokens)
}
tokens, err := receiver.amocrmService.Auth(ctx.Request().Context(), callbackCode)
if err != nil {
receiver.logger.Errorf("failed to amocrm auth on <Callback> of <AmocrmAuthController>: %v", err)
return utils.DetermineEchoErrorResponse(ctx, err, fmt.Sprintf("failed to auth: %v", err))
}
return ctx.JSON(http.StatusOK, tokens)
}

@ -0,0 +1,324 @@
package amocrm_test
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/labstack/echo"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/controller/amocrm"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/controller/amocrm/mocks"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/json"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/testifyhelper"
)
func TestAmocrmCallback(t *testing.T) {
code := "testCode"
accessToken := "accessttttoken"
testifyHelper := testifyhelper.NewEchoTestifyHelper()
tokens := models.Tokens{
AccessToken: "access-token",
RefreshToken: "refresh-token",
}
t.Run("Неверный state", func(t *testing.T) {
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
Headers: map[string]string{echo.HeaderContentType: echo.MIMEApplicationJSON},
QueryParams: map[string]string{
"state": "invalid_state",
"code": code,
},
})
oauthService.EXPECT().ValidateState("invalid_state").Return(false).Once()
assert.NoError(t, amocrmAuthController.Callback(preparedRequest.EchoContext))
assert.Equal(t, http.StatusBadRequest, preparedRequest.Recorder.Code)
})
t.Run("Сервис вернул ошибку (accessToken отсутствует)", func(t *testing.T) {
amocrmService := mocks.NewAmocrmService(t)
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AmocrmService: amocrmService,
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
Headers: map[string]string{echo.HeaderContentType: echo.MIMEApplicationJSON},
QueryParams: map[string]string{
"state": "random_state",
"code": code,
},
})
oauthService.EXPECT().ValidateState("random_state").Return(true).Once()
amocrmService.EXPECT().Auth(mock.Anything, code).Return(nil, errors.New("")).Once()
assert.NoError(t, amocrmAuthController.Callback(preparedRequest.EchoContext))
assert.Equal(t, http.StatusInternalServerError, preparedRequest.Recorder.Code)
})
t.Run("Сервис вернул ошибку (accessToken имеется)", func(t *testing.T) {
amocrmService := mocks.NewAmocrmService(t)
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AmocrmService: amocrmService,
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
Headers: map[string]string{echo.HeaderContentType: echo.MIMEApplicationJSON},
QueryParams: map[string]string{
"state": "state",
"code": code,
"accessToken": accessToken,
},
})
oauthService.EXPECT().ValidateState("state").Return(true).Once()
amocrmService.AssertNotCalled(t, "Auth")
amocrmService.EXPECT().Link(mock.Anything, code, accessToken).Return(false, errors.New("")).Once()
assert.NoError(t, amocrmAuthController.Callback(preparedRequest.EchoContext))
assert.Equal(t, http.StatusInternalServerError, preparedRequest.Recorder.Code)
})
t.Run("Сервис успешно отработал (accessToken отсутствует)", func(t *testing.T) {
amocrmService := mocks.NewAmocrmService(t)
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AmocrmService: amocrmService,
OAuthService: oauthService,
})
jsonBuffer, err := json.EncodeBuffer(tokens)
if err != nil {
t.Errorf("failed to encode json tokens: %v", err)
}
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
Body: bytes.NewReader(jsonBuffer.Bytes()),
Headers: map[string]string{echo.HeaderContentType: echo.MIMEApplicationJSON},
QueryParams: map[string]string{
"state": "some_state",
"code": code,
},
})
oauthService.EXPECT().ValidateState("some_state").Return(true).Once()
amocrmService.EXPECT().Auth(mock.Anything, code).Return(&tokens, nil).Once()
assert.NoError(t, amocrmAuthController.Callback(preparedRequest.EchoContext))
assert.Equal(t, http.StatusOK, preparedRequest.Recorder.Code)
})
t.Run("Сервис успешно отработал (accessToken имеется)", func(t *testing.T) {
amocrmService := mocks.NewAmocrmService(t)
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AmocrmService: amocrmService,
OAuthService: oauthService,
})
jsonBuffer, err := json.EncodeBuffer(tokens)
if err != nil {
t.Errorf("failed to encode json tokens: %v", err)
}
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
Body: bytes.NewReader(jsonBuffer.Bytes()),
Headers: map[string]string{echo.HeaderContentType: echo.MIMEApplicationJSON},
QueryParams: map[string]string{
"state": "login_state",
"code": code,
"accessToken": accessToken,
},
})
oauthService.EXPECT().ValidateState("login_state").Return(true).Once()
amocrmService.AssertNotCalled(t, "Auth")
amocrmService.EXPECT().Link(mock.Anything, code, accessToken).Return(true, nil).Once()
assert.NoError(t, amocrmAuthController.Callback(preparedRequest.EchoContext))
assert.Equal(t, http.StatusOK, preparedRequest.Recorder.Code)
})
}
func TestAmocrmGenerateAuthURL(t *testing.T) {
testifyHelper := testifyhelper.NewEchoTestifyHelper()
t.Run("Успешная генерация ссылки авторизации", func(t *testing.T) {
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
})
url := url.URL{
Scheme: "https",
Host: "www.amocrm.ru",
Path: "/oauth",
RawQuery: "access_type=offline&client_id=&response_type=code&state=555",
}
oauthService.EXPECT().GenerateAuthURL().Return(url.String()).Once()
assert.NoError(t, amocrmAuthController.GenerateAuthURL(preparedRequest.EchoContext))
unmarsled, err := json.Unmarshal[models.GenerateURLResponse](preparedRequest.Recorder.Body.Bytes())
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, preparedRequest.Recorder.Code)
assert.EqualValues(t, url.String(), unmarsled.URL)
})
}
func TestAmocrmRedirectAuthURL(t *testing.T) {
testifyHelper := testifyhelper.NewEchoTestifyHelper()
t.Run("Успешная генерация ссылки авторизации", func(t *testing.T) {
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
})
url := url.URL{
Scheme: "https",
Host: "www.amocrm.ru",
Path: "/oauth",
RawQuery: "access_type=offline&client_id=&response_type=code&state=555",
}
oauthService.EXPECT().GenerateAuthURL().Return(url.String()).Once()
assert.NoError(t, amocrmAuthController.RedirectAuthURL(preparedRequest.EchoContext))
assert.Equal(t, http.StatusTemporaryRedirect, preparedRequest.Recorder.Code)
result := preparedRequest.Recorder.Result()
if result != nil {
defer result.Body.Close()
}
if isNotNil := assert.NotNil(t, result); isNotNil {
redirectURL, err := result.Location()
assert.NoError(t, err)
assert.Equal(t, &url, redirectURL)
}
})
}
func TestAmocrmRedirectLinkAccountURL(t *testing.T) {
testifyHelper := testifyhelper.NewEchoTestifyHelper()
accessToken := "access-tokenasg"
redirectURL := url.URL{
Scheme: "https",
Host: "www.amocrm.ru",
Path: "/oauth",
RawQuery: fmt.Sprintf(
"accessToken=%s&access_type=offline&client_id=&response_type=code&state=555",
accessToken,
),
}
t.Run("Успешная генерация ссылки авторизации с токеном доступа", func(t *testing.T) {
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
QueryParams: map[string]string{"accessToken": accessToken},
})
oauthService.EXPECT().GenerateLinkURL(accessToken).Return(redirectURL.String()).Once()
assert.NoError(t, amocrmAuthController.RedirectLinkAccountURL(preparedRequest.EchoContext))
assert.Equal(t, http.StatusTemporaryRedirect, preparedRequest.Recorder.Code)
result := preparedRequest.Recorder.Result()
if result != nil {
defer result.Body.Close()
}
if isNotNil := assert.NotNil(t, result); isNotNil {
redirectURL, err := result.Location()
assert.NoError(t, err)
assert.Equal(t, redirectURL, redirectURL)
}
})
}
func TestAmocrmGenerateLinkAccountURL(t *testing.T) {
testifyHelper := testifyhelper.NewEchoTestifyHelper()
accessToken := "access-token"
redirectURL := url.URL{
Scheme: "https",
Host: "www.amocrm.ru",
Path: "/oauth",
RawQuery: fmt.Sprintf(
"accessToken=%s&access_type=offline&client_id=&response_type=code&state=555",
accessToken,
),
}
t.Run("Успешная генерация ссылки авторизации с токеном доступа", func(t *testing.T) {
oauthService := mocks.NewOauthService(t)
amocrmAuthController := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
OAuthService: oauthService,
})
preparedRequest := testifyHelper.PrepareRequest(testifyhelper.RequestConfiguration{
Method: http.MethodGet,
QueryParams: map[string]string{"accessToken": accessToken},
})
oauthService.EXPECT().GenerateLinkURL(accessToken).Return(redirectURL.String()).Once()
assert.NoError(t, amocrmAuthController.GenerateLinkAccountURL(preparedRequest.EchoContext))
assert.Equal(t, http.StatusOK, preparedRequest.Recorder.Code)
unmarsled, err := json.Unmarshal[models.GenerateURLResponse](preparedRequest.Recorder.Body.Bytes())
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, preparedRequest.Recorder.Code)
assert.EqualValues(t, redirectURL.String(), unmarsled.URL)
})
}

@ -0,0 +1,147 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
// AmocrmService is an autogenerated mock type for the amocrmService type
type AmocrmService struct {
mock.Mock
}
type AmocrmService_Expecter struct {
mock *mock.Mock
}
func (_m *AmocrmService) EXPECT() *AmocrmService_Expecter {
return &AmocrmService_Expecter{mock: &_m.Mock}
}
// Auth provides a mock function with given fields: ctx, code
func (_m *AmocrmService) Auth(ctx context.Context, code string) (*models.Tokens, error) {
ret := _m.Called(ctx, code)
var r0 *models.Tokens
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.Tokens, error)); ok {
return rf(ctx, code)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Tokens); ok {
r0 = rf(ctx, code)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tokens)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, code)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AmocrmService_Auth_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Auth'
type AmocrmService_Auth_Call struct {
*mock.Call
}
// Auth is a helper method to define mock.On call
// - ctx context.Context
// - code string
func (_e *AmocrmService_Expecter) Auth(ctx interface{}, code interface{}) *AmocrmService_Auth_Call {
return &AmocrmService_Auth_Call{Call: _e.mock.On("Auth", ctx, code)}
}
func (_c *AmocrmService_Auth_Call) Run(run func(ctx context.Context, code string)) *AmocrmService_Auth_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AmocrmService_Auth_Call) Return(_a0 *models.Tokens, _a1 error) *AmocrmService_Auth_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AmocrmService_Auth_Call) RunAndReturn(run func(context.Context, string) (*models.Tokens, error)) *AmocrmService_Auth_Call {
_c.Call.Return(run)
return _c
}
// Link provides a mock function with given fields: ctx, code, accessToken
func (_m *AmocrmService) Link(ctx context.Context, code string, accessToken string) (bool, error) {
ret := _m.Called(ctx, code, accessToken)
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok {
return rf(ctx, code, accessToken)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
r0 = rf(ctx, code, accessToken)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, code, accessToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AmocrmService_Link_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Link'
type AmocrmService_Link_Call struct {
*mock.Call
}
// Link is a helper method to define mock.On call
// - ctx context.Context
// - code string
// - accessToken string
func (_e *AmocrmService_Expecter) Link(ctx interface{}, code interface{}, accessToken interface{}) *AmocrmService_Link_Call {
return &AmocrmService_Link_Call{Call: _e.mock.On("Link", ctx, code, accessToken)}
}
func (_c *AmocrmService_Link_Call) Run(run func(ctx context.Context, code string, accessToken string)) *AmocrmService_Link_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *AmocrmService_Link_Call) Return(_a0 bool, _a1 error) *AmocrmService_Link_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AmocrmService_Link_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *AmocrmService_Link_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewAmocrmService interface {
mock.TestingT
Cleanup(func())
}
// NewAmocrmService creates a new instance of AmocrmService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewAmocrmService(t mockConstructorTestingTNewAmocrmService) *AmocrmService {
mock := &AmocrmService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,158 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// OauthService is an autogenerated mock type for the oauthService type
type OauthService struct {
mock.Mock
}
type OauthService_Expecter struct {
mock *mock.Mock
}
func (_m *OauthService) EXPECT() *OauthService_Expecter {
return &OauthService_Expecter{mock: &_m.Mock}
}
// GenerateAuthURL provides a mock function with given fields:
func (_m *OauthService) GenerateAuthURL() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// OauthService_GenerateAuthURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateAuthURL'
type OauthService_GenerateAuthURL_Call struct {
*mock.Call
}
// GenerateAuthURL is a helper method to define mock.On call
func (_e *OauthService_Expecter) GenerateAuthURL() *OauthService_GenerateAuthURL_Call {
return &OauthService_GenerateAuthURL_Call{Call: _e.mock.On("GenerateAuthURL")}
}
func (_c *OauthService_GenerateAuthURL_Call) Run(run func()) *OauthService_GenerateAuthURL_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *OauthService_GenerateAuthURL_Call) Return(_a0 string) *OauthService_GenerateAuthURL_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *OauthService_GenerateAuthURL_Call) RunAndReturn(run func() string) *OauthService_GenerateAuthURL_Call {
_c.Call.Return(run)
return _c
}
// GenerateLinkURL provides a mock function with given fields: accessToken
func (_m *OauthService) GenerateLinkURL(accessToken string) string {
ret := _m.Called(accessToken)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(accessToken)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// OauthService_GenerateLinkURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateLinkURL'
type OauthService_GenerateLinkURL_Call struct {
*mock.Call
}
// GenerateLinkURL is a helper method to define mock.On call
// - accessToken string
func (_e *OauthService_Expecter) GenerateLinkURL(accessToken interface{}) *OauthService_GenerateLinkURL_Call {
return &OauthService_GenerateLinkURL_Call{Call: _e.mock.On("GenerateLinkURL", accessToken)}
}
func (_c *OauthService_GenerateLinkURL_Call) Run(run func(accessToken string)) *OauthService_GenerateLinkURL_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *OauthService_GenerateLinkURL_Call) Return(_a0 string) *OauthService_GenerateLinkURL_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *OauthService_GenerateLinkURL_Call) RunAndReturn(run func(string) string) *OauthService_GenerateLinkURL_Call {
_c.Call.Return(run)
return _c
}
// ValidateState provides a mock function with given fields: state
func (_m *OauthService) ValidateState(state string) bool {
ret := _m.Called(state)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(state)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// OauthService_ValidateState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidateState'
type OauthService_ValidateState_Call struct {
*mock.Call
}
// ValidateState is a helper method to define mock.On call
// - state string
func (_e *OauthService_Expecter) ValidateState(state interface{}) *OauthService_ValidateState_Call {
return &OauthService_ValidateState_Call{Call: _e.mock.On("ValidateState", state)}
}
func (_c *OauthService_ValidateState_Call) Run(run func(state string)) *OauthService_ValidateState_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *OauthService_ValidateState_Call) Return(_a0 bool) *OauthService_ValidateState_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *OauthService_ValidateState_Call) RunAndReturn(run func(string) bool) *OauthService_ValidateState_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewOauthService interface {
mock.TestingT
Cleanup(func())
}
// NewOauthService creates a new instance of OauthService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewOauthService(t mockConstructorTestingTNewOauthService) *OauthService {
mock := &OauthService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,75 @@
package google
import (
"context"
"net/http"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
// TODO:
// 1) Необходимо вынести все ошибки в отдельный пакет
// 2) Реализовать map'у для возврата JSON ошибок
type GoogleClient interface {
GetUserInformation(ctx context.Context, accessToken string) (*models.GoogleUserInformation, error)
}
type Deps struct {
GoogleOAuthConfig *oauth2.Config
Client GoogleClient
Logger *logrus.Logger
}
type Controller struct {
oAuth *oauth2.Config
logger *logrus.Logger
client GoogleClient
state string
}
func New(deps *Deps) *Controller {
return &Controller{
oAuth: deps.GoogleOAuthConfig,
logger: deps.Logger,
client: deps.Client,
state: utils.GetRandomString(10),
}
}
func (receiver *Controller) Auth(ctx echo.Context) error {
url := receiver.oAuth.AuthCodeURL(receiver.state, oauth2.AccessTypeOffline)
return ctx.Redirect(http.StatusTemporaryRedirect, url)
}
func (receiver *Controller) Callback(ctx echo.Context) error {
callbackState := ctx.FormValue("state")
callbackCode := ctx.FormValue("code")
if callbackState != receiver.state {
receiver.logger.Errorln("state is not valid")
return ctx.JSON(http.StatusBadRequest, "state is not valid")
}
token, err := receiver.oAuth.Exchange(ctx.Request().Context(), callbackCode)
if err != nil {
receiver.logger.Errorln("exchange error: ", err.Error())
return ctx.JSON(http.StatusBadRequest, err.Error())
}
userInformation, err := receiver.client.GetUserInformation(ctx.Request().Context(), token.AccessToken)
if err != nil {
receiver.logger.Errorln("get user information error: ", err.Error())
return ctx.JSON(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, userInformation)
}

@ -0,0 +1,71 @@
package vk
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
type VKClient interface {
GetUserInformation(token *oauth2.Token) (*models.VKUserInformation, error)
}
type Deps struct {
VKOAuthConfig *oauth2.Config
Client VKClient
Logger *logrus.Logger
}
type Controller struct {
oAuth *oauth2.Config
logger *logrus.Logger
client VKClient
state string
}
func New(deps *Deps) *Controller {
return &Controller{
oAuth: deps.VKOAuthConfig,
logger: deps.Logger,
client: deps.Client,
state: utils.GetRandomString(10),
}
}
func (receiver *Controller) Auth(ctx echo.Context) error {
url := receiver.oAuth.AuthCodeURL(receiver.state, oauth2.AccessTypeOffline)
return ctx.Redirect(http.StatusTemporaryRedirect, url)
}
func (receiver *Controller) Callback(ctx echo.Context) error {
queries := ctx.Request().URL.Query()
callbackCode := queries.Get("code")
callbackState := queries.Get("state")
if callbackState != receiver.state {
receiver.logger.Errorln("state is not valid on <Callback> of <AmocrmController>")
return ctx.JSON(http.StatusBadRequest, "state is not valid")
}
token, err := receiver.oAuth.Exchange(ctx.Request().Context(), callbackCode)
if err != nil {
receiver.logger.Errorf("exchange error: %v", err)
return ctx.JSON(http.StatusBadRequest, err.Error())
}
userInformation, err := receiver.client.GetUserInformation(token)
if err != nil {
receiver.logger.Errorf("get user information error: %v", err)
return ctx.JSON(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, userInformation)
}

@ -0,0 +1,7 @@
package errors
import "errors"
var (
ErrNoServerItem = errors.New("microservice/service has no such item")
)

10
internal/errors/common.go Normal file

@ -0,0 +1,10 @@
package errors
import "errors"
var (
ErrInvalidReturnValue = errors.New("method of function returned invalid value")
ErrEmptyArgs = errors.New("empty arguments or nil argument")
ErrInvalidArgs = errors.New("invalid arguments")
ErrMethodNotImplemented = errors.New("method is not implemented")
)

@ -0,0 +1,15 @@
package errors
import "errors"
var (
ErrNoRecord = errors.New("no record in db")
ErrInsertRecord = errors.New("failed to insert record")
ErrReadRecord = errors.New("failed to read record")
ErrFindRecord = errors.New("failed to find record")
ErrDecodeRecord = errors.New("failed to decode structure")
ErrTransaction = errors.New("failed transaction")
ErrTransactionSessionStart = errors.New("failed to start transaction session")
ErrUpdateRecord = errors.New("failed to update record")
ErrRecordAlreadyExist = errors.New("record already exist")
)

15
internal/fields/amocrm.go Normal file

@ -0,0 +1,15 @@
package fields
var AmocrmUser = struct {
ID string
AmocrmID string
UserID string
Information string
Audit string
}{
ID: "id",
AmocrmID: "amocrmId",
UserID: "userId",
Information: "information",
Audit: auditFieldName,
}

17
internal/fields/audit.go Normal file

@ -0,0 +1,17 @@
package fields
import "fmt"
var auditFieldName = "audit"
var Audit = struct {
UpdatedAt string
DeletedAt string
CreatedAt string
Deleted string
}{
UpdatedAt: fmt.Sprintf("%s.updatedAt", auditFieldName),
DeletedAt: fmt.Sprintf("%s.deletedAt", auditFieldName),
CreatedAt: fmt.Sprintf("%s.createdAt", auditFieldName),
Deleted: fmt.Sprintf("%s.deleted", auditFieldName),
}

25
internal/fields/google.go Normal file

@ -0,0 +1,25 @@
package fields
import "fmt"
// TODO: актуализировать поля для google user'а
var GoogleFields = struct {
Subject string
Fullname string
GivenName string
FamilyName string
PictureURL string
Email string
EmailVerified string
Locale string
}{
Subject: fmt.Sprintf("%s.sub", UserFields.Google),
Fullname: fmt.Sprintf("%s.name", UserFields.Google),
GivenName: fmt.Sprintf("%s.given_name", UserFields.Google),
FamilyName: fmt.Sprintf("%s.family_name", UserFields.Google),
PictureURL: fmt.Sprintf("%s.picture", UserFields.Google),
Email: fmt.Sprintf("%s.email", UserFields.Google),
EmailVerified: fmt.Sprintf("%s.email_verified", UserFields.Google),
Locale: fmt.Sprintf("%s.locale", UserFields.Google),
}

19
internal/fields/user.go Normal file

@ -0,0 +1,19 @@
package fields
var UserFields = struct {
ID string
UserID string
GoogleID string
VKID string
Google string
VK string
Amocrm string
}{
ID: "_id",
UserID: "userId",
GoogleID: "googleId",
VKID: "vkId",
Google: "googleInformation",
VK: "vkInformation",
Amocrm: "amocrmInformation",
}

37
internal/fields/vk.go Normal file

@ -0,0 +1,37 @@
package fields
import "fmt"
// TODO: актуализировать поля для vk user'а
var VKFields = struct {
ID string
FirstName string
LastName string
Photo string
Sex string
Domain string
ScreenName string
Birthday string
PhotoID string
FollowersCount string
HomeTown string
Timezone string
MobilePhone string
Email string
}{
ID: fmt.Sprintf("%s.id", UserFields.VK),
FirstName: fmt.Sprintf("%s.firstname", UserFields.VK),
LastName: fmt.Sprintf("%s.lastname", UserFields.VK),
Photo: fmt.Sprintf("%s.avatar", UserFields.VK),
Sex: fmt.Sprintf("%s.sex", UserFields.VK),
Domain: fmt.Sprintf("%s.domain", UserFields.VK),
ScreenName: fmt.Sprintf("%s.screen_name", UserFields.VK),
Birthday: fmt.Sprintf("%s.birthday", UserFields.VK),
PhotoID: fmt.Sprintf("%s.photo_id", UserFields.VK),
FollowersCount: fmt.Sprintf("%s.followers_count", UserFields.VK),
HomeTown: fmt.Sprintf("%s.home_town", UserFields.VK),
Timezone: fmt.Sprintf("%s.timezone", UserFields.VK),
MobilePhone: fmt.Sprintf("%s.mobile_phone", UserFields.VK),
Email: fmt.Sprintf("%s.email", UserFields.VK),
}

@ -0,0 +1,46 @@
package initialize
import (
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/client"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
type ClientsDeps struct {
Logger *logrus.Logger
GoogleURL *models.GoogleURL
AmocrmURL *models.AmocrmURL
AuthURL *models.AuthMicroServiceURL
AmocrmOAuthConfiguration *oauth2.Config
}
type Clients struct {
GoogleClient *client.GoogleClient
VKClient *client.VKClient
AmocrmClient *client.AmocrmClient
AuthClient *client.AuthClient
AmocrmOAuthClient *client.OAuthClient
}
func NewClients(deps *ClientsDeps) *Clients {
return &Clients{
VKClient: client.NewVKClient(deps.Logger),
GoogleClient: client.NewGoogleClient(&client.GoogleClientDeps{
Logger: deps.Logger,
URLs: deps.GoogleURL,
}),
AmocrmClient: client.NewAmocrmClient(&client.AmocrmClientDeps{
Logger: deps.Logger,
URLs: deps.AmocrmURL,
}),
AuthClient: client.NewAuthClient(&client.AuthClientDeps{
Logger: deps.Logger,
URLs: deps.AuthURL,
}),
AmocrmOAuthClient: client.NewOAuthClient(&client.OAuthClientDeps{
Logger: deps.Logger,
Config: deps.AmocrmOAuthConfiguration,
}),
}
}

@ -0,0 +1,23 @@
package initialize_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
)
func TestNewClients(t *testing.T) {
t.Run("Клиенты должны успешно инициализироваться", func(t *testing.T) {
assert.NotPanics(t, func() {
clients := initialize.NewClients(&initialize.ClientsDeps{})
assert.NotNil(t, clients)
assert.NotNil(t, clients.AmocrmClient)
assert.NotNil(t, clients.AuthClient)
assert.NotNil(t, clients.GoogleClient)
assert.NotNil(t, clients.AmocrmOAuthClient)
assert.NotNil(t, clients.VKClient)
})
})
}

@ -0,0 +1,68 @@
package initialize
import (
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/vk"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/env"
)
func Configuration(path string) (*models.Config, error) {
config, err := env.Parse[models.Config](path)
if err != nil {
return nil, err
}
if err := utils.ValidateConfigurationURLs(&config.Service); err != nil {
return nil, err
}
initOAuth2Configuration(&config.Service)
iniJWTConfiguration(&config.Service.JWT)
return config, nil
}
func initOAuth2Configuration(config *models.ServiceConfiguration) {
config.Google.OAuthConfig = oauth2.Config{
RedirectURL: config.Google.URL.Redirect,
ClientID: config.Google.ClientID,
ClientSecret: config.Google.ClientSecret,
Scopes: []string{
"openid",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
config.VK.OAuthConfig = oauth2.Config{
RedirectURL: config.VK.URL.Redirect,
ClientID: config.VK.ClientID,
ClientSecret: config.VK.ClientSecret,
Scopes: []string{"email"},
Endpoint: vk.Endpoint,
}
config.Amocrm.OAuthConfig = oauth2.Config{
RedirectURL: config.Amocrm.URL.Redirect,
ClientID: config.Amocrm.ClientID,
ClientSecret: config.Amocrm.ClientSecret,
Scopes: nil,
Endpoint: oauth2.Endpoint{
AuthURL: config.Amocrm.URL.OAuthHost,
TokenURL: config.Amocrm.URL.AccessToken,
AuthStyle: models.BodyAuthStyle,
},
}
}
func iniJWTConfiguration(config *models.JWTConfiguration) {
config.Algorithm = *jwt.SigningMethodRS256
config.ExpiresIn = 15 * time.Minute
}

@ -0,0 +1,191 @@
package initialize_test
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/vk"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/mongo"
)
func setDefaultTestingENV(t *testing.T) *models.Config {
t.Helper()
defaultGoogleURL := models.GoogleURL{
Redirect: "http://www.google.com/callback",
OAuthHost: "http://www.google.com/oauth",
}
defaultVKURL := models.VKURL{
Redirect: "http://www.vk.ru/callback",
}
defaultAmocrmURL := models.AmocrmURL{
Redirect: "http://www.amocrm.ru/callback",
OAuthHost: "http://www.amocrm.ru/oauth",
UserInfo: "http://www.amocrm.ru/user",
AccessToken: "http://www.amocrm.ru/token",
}
defaultAuthURL := models.AuthMicroServiceURL{
Exchange: "http://www.auth.ru/callback",
Register: "http://www.auth.ru/register",
User: "http://www.auth.ru/user",
}
defaultGoogleOAuthConfiguration := oauth2.Config{
RedirectURL: defaultGoogleURL.Redirect,
ClientID: "google_client_id",
ClientSecret: "google_client_secret",
Scopes: []string{
"openid",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
defaultVKOAuthConfiguration := oauth2.Config{
RedirectURL: defaultVKURL.Redirect,
ClientID: "vk_client_id",
ClientSecret: "vk_client_secret",
Scopes: []string{"email"},
Endpoint: vk.Endpoint,
}
defaultAmocrmOAuthConfiguration := oauth2.Config{
RedirectURL: defaultAmocrmURL.Redirect,
ClientID: "amocrm_client_id",
ClientSecret: "amocrm_client_secret",
Scopes: nil,
Endpoint: oauth2.Endpoint{
AuthURL: defaultAmocrmURL.OAuthHost,
TokenURL: defaultAmocrmURL.AccessToken,
AuthStyle: models.BodyAuthStyle,
},
}
defaultConfiguration := models.Config{
HTTP: models.HTTPConfiguration{
Host: "localhost",
Port: "8080",
},
Service: models.ServiceConfiguration{
Google: models.GoogleConfiguration{
ClientID: defaultGoogleOAuthConfiguration.ClientID,
ClientSecret: defaultGoogleOAuthConfiguration.ClientSecret,
OAuthConfig: defaultGoogleOAuthConfiguration,
URL: defaultGoogleURL,
},
VK: models.VKConfiguration{
ClientID: defaultVKOAuthConfiguration.ClientID,
ClientSecret: defaultVKOAuthConfiguration.ClientSecret,
OAuthConfig: defaultVKOAuthConfiguration,
URL: defaultVKURL,
},
Amocrm: models.AmocrmConfiguration{
ClientID: defaultAmocrmOAuthConfiguration.ClientID,
ClientSecret: defaultAmocrmOAuthConfiguration.ClientSecret,
OAuthConfig: defaultAmocrmOAuthConfiguration,
URL: defaultAmocrmURL,
},
AuthMicroservice: models.AuthMicroserviceConfiguration{
AuthGroup: "group",
PrivateSignKey: "key",
URL: defaultAuthURL,
},
JWT: models.JWTConfiguration{
PrivateKey: "jwt private key",
PublicKey: "jwt public key",
Issuer: "issuer",
Audience: "audience",
Algorithm: *jwt.SigningMethodRS256,
ExpiresIn: 15 * time.Minute,
},
},
Database: mongo.Configuration{
Host: "localhost",
Port: "27017",
User: "user",
Password: "pass",
Auth: "db",
DatabaseName: "db",
},
}
t.Setenv("JWT_PUBLIC_KEY", defaultConfiguration.Service.JWT.PublicKey)
t.Setenv("JWT_PRIVATE_KEY", defaultConfiguration.Service.JWT.PrivateKey)
t.Setenv("JWT_ISSUER", defaultConfiguration.Service.JWT.Issuer)
t.Setenv("JWT_AUDIENCE", defaultConfiguration.Service.JWT.Audience)
t.Setenv("GOOGLE_CLIENT_ID", defaultConfiguration.Service.Google.ClientID)
t.Setenv("GOOGLE_CLIENT_SECRET", defaultConfiguration.Service.Google.ClientSecret)
t.Setenv("GOOGLE_REDIRECT_URL", defaultConfiguration.Service.Google.URL.Redirect)
t.Setenv("GOOGLE_OAUTH_HOST", defaultConfiguration.Service.Google.URL.OAuthHost)
t.Setenv("VK_CLIENT_ID", defaultConfiguration.Service.VK.ClientID)
t.Setenv("VK_CLIENT_SECRET", defaultConfiguration.Service.VK.ClientSecret)
t.Setenv("VK_REDIRECT_URL", defaultConfiguration.Service.VK.URL.Redirect)
t.Setenv("AMOCRM_CLIENT_ID", defaultConfiguration.Service.Amocrm.ClientID)
t.Setenv("AMOCRM_CLIENT_SECRET", defaultConfiguration.Service.Amocrm.ClientSecret)
t.Setenv("AMOCRM_REDIRECT_URL", defaultConfiguration.Service.Amocrm.URL.Redirect)
t.Setenv("AMOCRM_OAUTH_HOST", defaultConfiguration.Service.Amocrm.URL.OAuthHost)
t.Setenv("AMOCRM_USER_INFO_URL", defaultConfiguration.Service.Amocrm.URL.UserInfo)
t.Setenv("AMOCRM_ACCESS_TOKEN_URL", defaultConfiguration.Service.Amocrm.URL.AccessToken)
t.Setenv("AUTH_MICROSERVICE_GROUP", defaultConfiguration.Service.AuthMicroservice.AuthGroup)
t.Setenv("AUTH_MICROSERVICE_PRIVATE_SIGN_KEY", defaultConfiguration.Service.AuthMicroservice.PrivateSignKey)
t.Setenv("AUTH_MICROSERVICE_EXHANGE_URL", defaultConfiguration.Service.AuthMicroservice.URL.Exchange)
t.Setenv("AUTH_MICROSERVICE_REGISTER_URL", defaultConfiguration.Service.AuthMicroservice.URL.Register)
t.Setenv("AUTH_MICROSERVICE_USER_URL", defaultConfiguration.Service.AuthMicroservice.URL.User)
t.Setenv("MONGO_HOST", defaultConfiguration.Database.Host)
t.Setenv("MONGO_PORT", defaultConfiguration.Database.Port)
t.Setenv("MONGO_USER", defaultConfiguration.Database.User)
t.Setenv("MONGO_PASSWORD", defaultConfiguration.Database.Password)
t.Setenv("MONGO_AUTH", defaultConfiguration.Database.Auth)
t.Setenv("MONGO_DB_NAME", defaultConfiguration.Database.DatabaseName)
return &defaultConfiguration
}
func TestConfiguration(t *testing.T) {
t.Run("Успешная инициализация конфигурации", func(t *testing.T) {
defaultConfiguration := setDefaultTestingENV(t)
assert.NotPanics(t, func() {
configuration, err := initialize.Configuration("")
assert.NoError(t, err)
assert.Equal(t, defaultConfiguration, configuration)
})
})
t.Run("Ошибка при наличии кривого url", func(t *testing.T) {
setDefaultTestingENV(t)
t.Setenv("AMOCRM_USER_INFO_URL", "url")
assert.NotPanics(t, func() {
configuration, err := initialize.Configuration("")
assert.Error(t, err)
assert.Nil(t, configuration)
})
})
t.Run("Ошибка при отсутствии обязательного env", func(t *testing.T) {
assert.NotPanics(t, func() {
configuration, err := initialize.Configuration("")
assert.Error(t, err)
assert.Nil(t, configuration)
})
})
}

@ -0,0 +1,31 @@
package initialize
import (
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/controller/amocrm"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/controller/google"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/controller/vk"
)
type ControllersDeps struct {
Logger *logrus.Logger
Services *Services
}
type Controllers struct {
GoogleController *google.Controller
VKController *vk.Controller
AmocrmController *amocrm.Controller
}
func NewControllers(deps *ControllersDeps) *Controllers {
return &Controllers{
AmocrmController: amocrm.New(&amocrm.Deps{
Logger: deps.Logger,
OAuthService: deps.Services.AmocrmOAuthService,
AmocrmService: deps.Services.AmocrmService,
}),
VKController: vk.New(&vk.Deps{}),
GoogleController: google.New(&google.Deps{}),
}
}

@ -0,0 +1,23 @@
package initialize_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
)
func TestNewControllers(t *testing.T) {
t.Run("Контроллеры должны успешно инициализироваться", func(t *testing.T) {
assert.NotPanics(t, func() {
controllers := initialize.NewControllers(&initialize.ControllersDeps{
Services: &initialize.Services{},
})
assert.NotNil(t, controllers)
assert.NotNil(t, controllers.AmocrmController)
assert.NotNil(t, controllers.GoogleController)
assert.NotNil(t, controllers.VKController)
})
})
}

@ -0,0 +1,32 @@
package initialize
import (
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/repository"
)
type RepositoriesDeps struct {
MongoDB *mongo.Database
Logger *logrus.Logger
}
type Repositories struct {
HealthRepository *repository.HealthRepository
GoogleRepository *repository.GoogleRepository
AmocrmRepository *repository.AmocrmRepository
}
func NewRepositories(deps *RepositoriesDeps) *Repositories {
return &Repositories{
HealthRepository: repository.NewHealthRepository(deps.MongoDB),
AmocrmRepository: repository.NewAmocrmRepository(
deps.MongoDB.Collection("amocrm"),
deps.Logger,
),
GoogleRepository: repository.NewGoogleRepository(
deps.MongoDB.Collection("google"),
deps.Logger,
),
}
}

@ -0,0 +1,26 @@
package initialize_test
import (
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
)
func TestNewRepositories(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("Репозитории должны успешно инициализироваться", func(t *mtest.T) {
assert.NotPanics(t, func() {
repositories := initialize.NewRepositories(&initialize.RepositoriesDeps{
MongoDB: t.Client.Database("test"),
})
assert.NotNil(t, repositories)
assert.NotNil(t, repositories.AmocrmRepository)
assert.NotNil(t, repositories.GoogleRepository)
assert.NotNil(t, repositories.HealthRepository)
})
})
}

@ -0,0 +1,51 @@
package initialize
import (
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/amocrm"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/auth"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/encrypt"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/oauth"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
)
type ServicesDeps struct {
Logger *logrus.Logger
Config *models.ServiceConfiguration
Repositories *Repositories
Clients *Clients
}
type Services struct {
AmocrmService *amocrm.Service
AmocrmOAuthService *oauth.Service[models.AmocrmUserInformation]
}
func NewServices(deps *ServicesDeps) *Services {
authService := auth.New(&auth.Deps{
Logger: deps.Logger,
AuthClient: deps.Clients.AuthClient,
EncryptionService: encrypt.New(&encrypt.ServiceDeps{
JWT: utils.NewJWT[models.JWTAuthUser](&deps.Config.JWT),
PrivateCurveKey: deps.Config.AuthMicroservice.PrivateSignKey,
SignSecret: deps.Config.AuthMicroservice.AuthGroup,
}),
})
amocrmOAuthService := oauth.New(&oauth.Deps[models.AmocrmUserInformation]{
Logger: deps.Logger,
ServiceClient: deps.Clients.AmocrmClient,
OAuthClient: deps.Clients.AmocrmOAuthClient,
})
return &Services{
AmocrmOAuthService: amocrmOAuthService,
AmocrmService: amocrm.New(&amocrm.Deps{
Logger: deps.Logger,
AuthService: authService,
Repository: deps.Repositories.AmocrmRepository,
OAuthService: amocrmOAuthService,
}),
}
}

@ -0,0 +1,26 @@
package initialize_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
)
func TestNewServices(t *testing.T) {
configuration := setDefaultTestingENV(t)
t.Run("Сервисы должны успешно инициализироваться", func(t *testing.T) {
assert.NotPanics(t, func() {
services := initialize.NewServices(&initialize.ServicesDeps{
Config: &configuration.Service,
Clients: &initialize.Clients{},
Repositories: &initialize.Repositories{},
})
assert.NotNil(t, services)
assert.NotNil(t, services.AmocrmOAuthService)
assert.NotNil(t, services.AmocrmService)
})
})
}

103
internal/models/amocrm.go Normal file

@ -0,0 +1,103 @@
package models
type AmocrmUser struct {
ID string `json:"id" bson:"_id,omitempty"`
AmocrmID string `json:"amocrmId" bson:"amocrmId"`
UserID string `json:"userId,omitempty" bson:"userId,omitempty"`
Information AmocrmUserInformation `json:"information" bson:"information"`
Audit Audit `json:"audit" bson:"audit"`
}
type AmocrmUserInformation struct {
ID int64 `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
Subdomain string `json:"subdomain" bson:"subdomain"`
CreatedAt int `json:"created_at" bson:"created_at"`
CreatedBy int `json:"created_by" bson:"created_by"`
UpdatedAt int `json:"updated_at" bson:"updated_at"`
UpdatedBy int `json:"updated_by" bson:"updated_by"`
CurrentUserID int `json:"current_user_id" bson:"current_user_id"`
Country string `json:"country" bson:"country"`
CustomersMode string `json:"customers_mode" bson:"customers_mode"`
IsUnsortedOn bool `json:"is_unsorted_on" bson:"is_unsorted_on"`
IsLossReasonEnabled bool `json:"is_loss_reason_enabled" bson:"is_loss_reason_enabled"`
IsHelpbotEnabled bool `json:"is_helpbot_enabled" bson:"is_helpbot_enabled"`
IsTechnicalAccount bool `json:"is_technical_account" bson:"is_technical_account"`
ContactNameDisplayOrder int `json:"contact_name_display_order" bson:"contact_name_display_order"`
AmojoID string `json:"amojo_id" bson:"amojo_id"`
UUID string `json:"uuid" bson:"uuid"`
Version int `json:"version" bson:"version"`
Links struct {
Self struct {
Href string `json:"href" bson:"href"`
} `json:"self" bson:"self"`
} `json:"_links" bson:"_links"`
Embedded struct {
AmojoRights struct {
CanDirect bool `json:"can_direct" bson:"can_direct"`
CanCreateGroups bool `json:"can_create_groups" bson:"can_create_groups"`
} `json:"amojo_rights" bson:"amojo_rights"`
UsersGroups []struct {
ID int `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
UUID interface{} `json:"uuid" bson:"uuid"`
} `json:"users_groups" bson:"users_groups"`
TaskTypes []struct {
ID int `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
Color interface{} `json:"color" bson:"color"`
IconID interface{} `json:"icon_id" bson:"icon_id"`
Code string `json:"code" bson:"code"`
} `json:"task_types" bson:"task_types"`
EntityNames struct {
Leads struct {
Ru struct {
Gender string `json:"gender" bson:"gender"`
PluralForm struct {
Dative string `json:"dative" bson:"dative"`
Default string `json:"default" bson:"default"`
Genitive string `json:"genitive" bson:"genitive"`
Accusative string `json:"accusative" bson:"accusative"`
Instrumental string `json:"instrumental" bson:"instrumental"`
Prepositional string `json:"prepositional" bson:"prepositional"`
} `json:"plural_form" bson:"plural_form"`
SingularForm struct {
Dative string `json:"dative" bson:"dative"`
Default string `json:"default" bson:"default"`
Genitive string `json:"genitive" bson:"genitive"`
Accusative string `json:"accusative" bson:"accusative"`
Instrumental string `json:"instrumental" bson:"instrumental"`
Prepositional string `json:"prepositional" bson:"prepositional"`
} `json:"singular_form" bson:"singular_form"`
} `json:"ru" bson:"ru"`
En struct {
SingularForm struct {
Default string `json:"default" bson:"default"`
} `json:"singular_form" bson:"singular_form"`
PluralForm struct {
Default string `json:"default" bson:"default"`
} `json:"plural_form" bson:"plural_form"`
Gender string `json:"gender" bson:"gender"`
} `json:"en" bson:"en"`
Es struct {
SingularForm struct {
Default string `json:"default" bson:"default"`
} `json:"singular_form" bson:"singular_form"`
PluralForm struct {
Default string `json:"default" bson:"default"`
} `json:"plural_form" bson:"plural_form"`
Gender string `json:"gender" bson:"gender"`
} `json:"es" bson:"es"`
} `json:"leads" bson:"leads"`
} `json:"entity_names" bson:"entity_names"`
DatetimeSettings struct {
DatePattern string `json:"date_pattern" bson:"date_pattern"`
ShortDatePattern string `json:"short_date_pattern" bson:"short_date_pattern"`
ShortTimePattern string `json:"short_time_pattern" bson:"short_time_pattern"`
DateFormant string `json:"date_formant" bson:"date_formant"`
TimeFormat string `json:"time_format" bson:"time_format"`
Timezone string `json:"timezone" bson:"timezone"`
TimezoneOffset string `json:"timezone_offset" bson:"timezone_offset"`
} `json:"datetime_settings" bson:"datetime_settings"`
} `json:"_embedded" bson:"_embedded"`
}

40
internal/models/auth.go Normal file

@ -0,0 +1,40 @@
package models
import "time"
type AuthUserInformation struct {
Email string `json:"email"`
PhoneNumber string `json:"phoneNumber"`
}
type JWTAuthUser struct {
ID string `json:"id"`
}
type AuthUser struct {
ID string `json:"_id"`
Login string `json:"login"`
Email string `json:"email"`
PhoneNumber string `json:"phoneNumber"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
IsDeleted bool `json:"isDeleted"`
}
type Tokens struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
type RegisterRequest struct {
Login string `json:"login"`
PhoneNumber string `json:"phoneNumber"`
Email string `json:"email"`
Password string `json:"password"`
}
type ExchangeRequest struct {
UserID string `json:"userId"`
Signature string `json:"signature"`
}

33
internal/models/common.go Normal file

@ -0,0 +1,33 @@
package models
import (
"time"
"golang.org/x/oauth2"
)
type Audit struct {
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
Deleted bool `json:"deleted" bson:"deleted"`
}
type GenerateURLResponse struct {
URL string `json:"url"`
}
type FastifyError struct {
StatusCode int `json:"statusCode"`
Error string `json:"error"`
Message string `json:"message"`
}
type ResponseErrorHTTP struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
}
const (
BodyAuthStyle oauth2.AuthStyle = 4
)

87
internal/models/config.go Normal file

@ -0,0 +1,87 @@
package models
import (
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/mongo"
)
type Config struct {
HTTP HTTPConfiguration
Service ServiceConfiguration
Database mongo.Configuration
}
type HTTPConfiguration struct {
Host string `env:"HTTP_HOST,default=localhost"`
Port string `env:"HTTP_PORT,default=8080"`
}
type ServiceConfiguration struct {
Google GoogleConfiguration
VK VKConfiguration
Amocrm AmocrmConfiguration
AuthMicroservice AuthMicroserviceConfiguration
JWT JWTConfiguration
}
type JWTConfiguration struct {
PrivateKey string `env:"JWT_PRIVATE_KEY"`
PublicKey string `env:"JWT_PUBLIC_KEY,required"`
Issuer string `env:"JWT_ISSUER,required"`
Audience string `env:"JWT_AUDIENCE,required"`
Algorithm jwt.SigningMethodRSA
ExpiresIn time.Duration
}
type GoogleConfiguration struct {
ClientID string `env:"GOOGLE_CLIENT_ID,required"`
ClientSecret string `env:"GOOGLE_CLIENT_SECRET,required"`
OAuthConfig oauth2.Config
URL GoogleURL
}
type VKConfiguration struct {
ClientID string `env:"VK_CLIENT_ID,required"`
ClientSecret string `env:"VK_CLIENT_SECRET,required"`
OAuthConfig oauth2.Config
URL VKURL
}
type AmocrmConfiguration struct {
ClientID string `env:"AMOCRM_CLIENT_ID,required"`
ClientSecret string `env:"AMOCRM_CLIENT_SECRET,required"`
OAuthConfig oauth2.Config
URL AmocrmURL
}
type AuthMicroserviceConfiguration struct {
AuthGroup string `env:"AUTH_MICROSERVICE_GROUP,required"`
PrivateSignKey string `env:"AUTH_MICROSERVICE_PRIVATE_SIGN_KEY,required"`
URL AuthMicroServiceURL
}
type GoogleURL struct {
Redirect string `env:"GOOGLE_REDIRECT_URL,required"`
OAuthHost string `env:"GOOGLE_OAUTH_HOST,required"`
UserInfo string
}
type AmocrmURL struct {
Redirect string `env:"AMOCRM_REDIRECT_URL,required"`
OAuthHost string `env:"AMOCRM_OAUTH_HOST,required"`
UserInfo string `env:"AMOCRM_USER_INFO_URL,required"`
AccessToken string `env:"AMOCRM_ACCESS_TOKEN_URL,required"`
}
type AuthMicroServiceURL struct {
Exchange string `env:"AUTH_MICROSERVICE_EXHANGE_URL,required"`
Register string `env:"AUTH_MICROSERVICE_REGISTER_URL,required"`
User string `env:"AUTH_MICROSERVICE_USER_URL,required"`
}
type VKURL struct {
Redirect string `env:"VK_REDIRECT_URL,required"`
}

13
internal/models/google.go Normal file

@ -0,0 +1,13 @@
package models
type GoogleUserInformation struct {
// The subject property contains the unique user identifier of the user who signed in
Subject string `json:"sub" bson:"Subject"`
Fullname string `json:"name" bson:"Fullname"`
GivenName string `json:"given_name" bson:"GivenName"`
FamilyName string `json:"family_name" bson:"FamilyName"`
AvatarURL string `json:"picture" bson:"AvatarURL"`
Email string `json:"email" bson:"Email"`
EmailVerified bool `json:"email_verified" bson:"EmailVerified"`
Locale string `json:"locale" bson:"Locale"`
}

18
internal/models/vk.go Normal file

@ -0,0 +1,18 @@
package models
type VKUserInformation struct {
ID int64 `json:"id" bson:"id"`
FirstName string `json:"first_name" bson:"firstname"`
LastName string `json:"last_name" bson:"lastname"`
Photo string `json:"photo_400_orig" bson:"avatar"`
Sex int `json:"sex" bson:"sex"`
Domain string `json:"domain" bson:"domain"`
ScreenName string `json:"screen_name" bson:"screen_name"`
Birthday string `json:"bdate" bson:"birthday"`
PhotoID string `json:"photo_id" bson:"photo_id"`
FollowersCount int `json:"followers_count" bson:"followers_count"`
HomeTown string `json:"home_town" bson:"home_town"`
Timezone float64 `json:"timezone" bson:"timezone"`
MobilePhone string `json:"mobile_phone" bson:"mobile_phone"`
Email string `json:"email" bson:"email"`
}

@ -0,0 +1,142 @@
package repository
import (
"context"
"time"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/fields"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
mongoWrapper "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/mongo"
)
type AmocrmRepository struct {
mongoDB *mongo.Collection
logger *logrus.Logger
}
func NewAmocrmRepository(mongoDB *mongo.Collection, logger *logrus.Logger) *AmocrmRepository {
return &AmocrmRepository{
mongoDB: mongoDB,
logger: logger,
}
}
func (receiver *AmocrmRepository) FindByID(ctx context.Context, amocrmID string) (*models.AmocrmUser, error) {
filter := bson.M{
fields.AmocrmUser.AmocrmID: amocrmID,
fields.Audit.Deleted: false,
}
user, err := mongoWrapper.FindOne[models.AmocrmUser](ctx, &mongoWrapper.RequestSettings{
Driver: receiver.mongoDB,
Filter: filter,
})
if err != nil {
receiver.logger.Errorf("failed to find amocrm user <%s> on <FindByID> of <AmocrmRepository>: %v", amocrmID, err)
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNoRecord
}
return nil, errors.ErrFindRecord
}
return user, nil
}
func (receiver *AmocrmRepository) FindByUserID(ctx context.Context, userID string) (*models.AmocrmUser, error) {
filter := bson.M{
fields.AmocrmUser.UserID: userID,
fields.Audit.Deleted: false,
}
user, err := mongoWrapper.FindOne[models.AmocrmUser](ctx, &mongoWrapper.RequestSettings{
Driver: receiver.mongoDB,
Filter: filter,
})
if err != nil {
receiver.logger.Errorf("failed to find amocrm user <%s> on <FindByUserID> of <AmocrmRepository>: %v", userID, err)
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNoRecord
}
return nil, errors.ErrFindRecord
}
return user, nil
}
func (receiver *AmocrmRepository) Insert(ctx context.Context, user *models.AmocrmUser) (*models.AmocrmUser, error) {
user.Audit = models.Audit{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Deleted: false,
}
result, err := receiver.mongoDB.InsertOne(ctx, user)
if err != nil {
receiver.logger.Errorf("failed to insert record on <Insert> of <AmocrmRepository>: %v", err)
return nil, errors.ErrInsertRecord
}
insertedID := result.InsertedID.(primitive.ObjectID).Hex()
userCopy := *user
userCopy.ID = insertedID
return &userCopy, nil
}
func (receiver *AmocrmRepository) Delete(ctx context.Context, amocrmID string) (*models.AmocrmUser, error) {
user := models.AmocrmUser{}
update := bson.M{"$set": bson.M{fields.Audit.Deleted: true}}
filter := bson.M{
fields.AmocrmUser.AmocrmID: amocrmID,
fields.Audit.Deleted: false,
}
if err := receiver.mongoDB.FindOneAndUpdate(ctx, filter, update).Decode(&user); err != nil {
receiver.logger.Errorf("failed to set 'deleted=true' with id <%s> on <Delete> of <AmocrmRepository>: %v", amocrmID, err)
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNoRecord
}
return nil, errors.ErrUpdateRecord
}
return &user, nil
}
func (receiver *AmocrmRepository) Remove(ctx context.Context, id string) (*models.AmocrmUser, error) {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
receiver.logger.Errorf("failed to parse ObjectID <%s> on <FindByID> of <DiscountRepository>: %v", id, err)
return nil, errors.ErrInvalidArgs
}
user := models.AmocrmUser{}
filter := bson.M{
fields.AmocrmUser.ID: objectID,
fields.Audit.Deleted: false,
}
if err := receiver.mongoDB.FindOneAndDelete(ctx, filter).Decode(&user); err != nil {
receiver.logger.Errorf("failed remove user with _id <%s> on <Remove> of <AmocrmRepository>: %v", id, err)
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNoRecord
}
return nil, errors.ErrUpdateRecord
}
return &user, nil
}

@ -0,0 +1,48 @@
package repository
import (
"context"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
type GoogleRepository struct {
mongoDB *mongo.Collection
logger *logrus.Logger
}
func NewGoogleRepository(mongoDB *mongo.Collection, logger *logrus.Logger) *GoogleRepository {
return &GoogleRepository{
mongoDB: mongoDB,
logger: logger,
}
}
func (receiver *GoogleRepository) UpdateUser(ctx context.Context, user *models.GoogleUserInformation) (*models.GoogleUserInformation, error) {
var googleUserInformation models.GoogleUserInformation
updateOptions := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After)
filter := bson.D{{Key: "sub", Value: user.Subject}}
update := bson.D{{Key: "$set", Value: user}}
if err := receiver.mongoDB.FindOneAndUpdate(ctx, filter, update, updateOptions).Decode(&googleUserInformation); err != nil {
receiver.logger.Errorf("failed decode google user information: %v", err)
return nil, errors.ErrDecodeRecord
}
return &googleUserInformation, nil
}
func (receiver *GoogleRepository) FindUserBySubject(_ context.Context, _ string) (*models.GoogleUserInformation, error) {
return nil, errors.ErrMethodNotImplemented
}
func (receiver *GoogleRepository) InsertUser(_ context.Context, _ *models.GoogleUserInformation) (*models.GoogleUserInformation, error) {
return nil, errors.ErrMethodNotImplemented
}

@ -0,0 +1,19 @@
package repository
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
)
type HealthRepository struct {
mongoDB *mongo.Database
}
func NewHealthRepository(database *mongo.Database) *HealthRepository {
return &HealthRepository{mongoDB: database}
}
func (receiver *HealthRepository) Check(ctx context.Context) error {
return receiver.mongoDB.Client().Ping(ctx, nil)
}

77
internal/server/http.go Normal file

@ -0,0 +1,77 @@
package server
import (
"context"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/initialize"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
type HTTP struct {
logger *logrus.Entry
server *http.Server
echo *echo.Echo
}
func New(logger *logrus.Logger) *HTTP {
echo := echo.New()
echo.Use(middleware.Logger())
echo.Use(middleware.Recover())
return &HTTP{
echo: echo,
logger: logrus.NewEntry(logger),
server: &http.Server{
Handler: echo,
MaxHeaderBytes: 1 << 20,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
}
}
func (receiver *HTTP) Listen(address string) error {
receiver.server.Addr = address
return receiver.server.ListenAndServe()
}
func (receiver *HTTP) Run(config *models.HTTPConfiguration) {
connectionString := fmt.Sprintf("%s:%s", config.Host, config.Port)
startServerMessage := fmt.Sprintf("starting http server on %s", connectionString)
receiver.logger.Infoln(startServerMessage)
if err := receiver.Listen(connectionString); err != nil && err != http.ErrServerClosed {
receiver.logger.Infoln("http listen error: ", err)
}
}
func (receiver *HTTP) Stop(ctx context.Context) error {
receiver.logger.Infoln("shutting down server ...")
if err := receiver.server.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown server: %w", err)
}
return nil
}
func (receiver *HTTP) Register(controllers *initialize.Controllers) *HTTP {
groupAmocrm := receiver.echo.Group("/amocrm")
groupAmocrm.GET("/auth/redirect", controllers.AmocrmController.RedirectAuthURL)
groupAmocrm.GET("/auth", controllers.AmocrmController.GenerateAuthURL)
groupAmocrm.GET("/link/redirect", controllers.AmocrmController.RedirectLinkAccountURL)
groupAmocrm.GET("/link", controllers.AmocrmController.GenerateLinkAccountURL)
groupAmocrm.GET("/callback", controllers.AmocrmController.Callback)
return receiver
}

@ -0,0 +1,131 @@
package amocrm
import (
"context"
"strconv"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
//go:generate mockery --name oauthService
type oauthService[T models.AmocrmUserInformation] interface {
GetUserInformationByCode(ctx context.Context, code string) (*T, error)
}
//go:generate mockery --name authService
type authService interface {
Register(ctx context.Context, information *models.AuthUserInformation) (*models.Tokens, string, error)
Login(ctx context.Context, userID string) (*models.Tokens, error)
GetAuthUserByToken(ctx context.Context, accessToken string) (*models.AuthUser, error)
}
//go:generate mockery --name amocrmRepository
type amocrmRepository interface {
FindByID(ctx context.Context, amocrmID string) (*models.AmocrmUser, error)
FindByUserID(ctx context.Context, userID string) (*models.AmocrmUser, error)
Insert(ctx context.Context, user *models.AmocrmUser) (*models.AmocrmUser, error)
}
type Deps struct {
Logger *logrus.Logger
Repository amocrmRepository
OAuthService oauthService[models.AmocrmUserInformation]
AuthService authService
}
type Service struct {
logger *logrus.Logger
oauthService oauthService[models.AmocrmUserInformation]
authService authService
repository amocrmRepository
}
func New(deps *Deps) *Service {
return &Service{
oauthService: deps.OAuthService,
logger: deps.Logger,
authService: deps.AuthService,
repository: deps.Repository,
}
}
func (receiver *Service) Link(ctx context.Context, code, accessToken string) (bool, error) {
authUser, err := receiver.authService.GetAuthUserByToken(ctx, accessToken)
if err != nil {
receiver.logger.Errorf("failed to get auth user on <Link> of <AmocrmService>: %v", err)
return false, err
}
amocrmUserInformation, err := receiver.oauthService.GetUserInformationByCode(ctx, code)
if err != nil {
receiver.logger.Errorf("failed to get amocrm user information on <Link> of <AmocrmService>: %v", err)
return false, err
}
amocrmUserID := strconv.Itoa(int(amocrmUserInformation.ID))
amocrmUser, err := receiver.repository.FindByID(ctx, amocrmUserID)
if err != nil && err != errors.ErrNoRecord {
receiver.logger.Errorf("failed to find user by amocrm id on <Link> of <AmocrmService>: %v", err)
return false, err
}
if amocrmUser != nil {
return false, errors.ErrRecordAlreadyExist
}
if _, err := receiver.repository.Insert(ctx, &models.AmocrmUser{
UserID: authUser.ID,
AmocrmID: amocrmUserID,
Information: *amocrmUserInformation,
}); err != nil {
receiver.logger.Errorf("failed to insert amocrm user on <Link> of <AmocrmService>: %v", err)
return false, err
}
return true, nil
}
func (receiver *Service) Auth(ctx context.Context, code string) (*models.Tokens, error) {
amocrmUserInformation, err := receiver.oauthService.GetUserInformationByCode(ctx, code)
if err != nil {
receiver.logger.Errorf("failed to get amocrm user information on <Auth> of <AmocrmService>: %v", err)
return nil, err
}
amocrmUserID := strconv.Itoa(int(amocrmUserInformation.ID))
amocrmUser, err := receiver.repository.FindByID(ctx, amocrmUserID)
if err != nil && err != errors.ErrNoRecord {
receiver.logger.Errorf("failed to find amocrm user by id on <Auth> of <AmocrmService>: %v", err)
return nil, err
}
if amocrmUser == nil {
tokens, createdUserID, registerErr := receiver.authService.Register(ctx, &models.AuthUserInformation{})
if registerErr != nil {
receiver.logger.Errorf("failed to register amocrm user on <Auth> of <AmocrmService>: %v", err)
return nil, registerErr
}
if _, insertErr := receiver.repository.Insert(ctx, &models.AmocrmUser{
UserID: createdUserID,
AmocrmID: amocrmUserID,
Information: *amocrmUserInformation,
}); insertErr != nil {
receiver.logger.Errorf("failed to insert user on <Auth> of <AmocrmService>: %v", err)
return nil, insertErr
}
return tokens, nil
}
tokens, err := receiver.authService.Login(ctx, amocrmUser.UserID)
if err != nil {
receiver.logger.Errorf("failed to login amocrm user on <Auth> of <AmocrmService>: %v", err)
return nil, err
}
return tokens, nil
}

@ -0,0 +1,522 @@
package amocrm_test
import (
"context"
"strconv"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/amocrm"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/amocrm/mocks"
)
var (
inputCode = "input-code"
amocrmUserInformation = models.AmocrmUserInformation{
ID: 2,
UUID: "geagae0-13413g-gadg",
}
amocrmUser = models.AmocrmUser{
UserID: "153-35-5135",
Information: amocrmUserInformation,
}
authUser = models.AuthUser{
ID: "1geat",
Login: "gkohoeh",
}
authTokens = models.Tokens{
AccessToken: "auth-251-access-token-auth",
RefreshToken: "auth-7582-refresh-token-auth",
}
)
func TestAmocrmLink(t *testing.T) {
t.Run("Успешная привязка аккаунта amocrm", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getAuthUserCall := authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, nil).
Once()
getUserInformationCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
findByAmocrmIDCall := amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrNoRecord).
NotBefore(getUserInformationCall).
Once()
amocrmRepository.EXPECT().
Insert(mock.Anything, &models.AmocrmUser{
UserID: authUser.ID,
AmocrmID: strconv.Itoa(int(amocrmUserInformation.ID)),
Information: amocrmUserInformation,
}).
Return(&amocrmUser, nil).
NotBefore(getAuthUserCall, findByAmocrmIDCall).
Once()
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.NoError(t, err)
assert.Equal(t, true, isLinked)
})
t.Run("Ошибка получения пользователя ЕСА по токену при привязке аккаунта amocrm", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, errors.ErrInvalidArgs).
Once()
oauthService.AssertNotCalled(t, "GetUserInformationByCode")
amocrmRepository.AssertNotCalled(t, "FindByID")
amocrmRepository.AssertNotCalled(t, "Insert")
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInvalidArgs.Error())
assert.Equal(t, false, isLinked)
})
t.Run("Ошибка получения пользователя amocrm по коду при привязке аккаунта amocrm", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, nil).
Once()
oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(nil, errors.ErrInvalidArgs).
Once()
amocrmRepository.AssertNotCalled(t, "FindByID")
amocrmRepository.AssertNotCalled(t, "Insert")
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInvalidArgs.Error())
assert.Equal(t, false, isLinked)
})
t.Run("Ошибка привязки аккаунта amocrm: пользователь уже существует", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getAuthUserCall := authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, nil).
Once()
getUserInformationCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
NotBefore(getAuthUserCall).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(&amocrmUser, nil).
NotBefore(getUserInformationCall).
Once()
amocrmRepository.AssertNotCalled(t, "Insert")
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrRecordAlreadyExist.Error())
assert.Equal(t, false, isLinked)
})
t.Run("Неизвестная ошибка при поиске пользователя amocrm в БД при привязке аккаунта", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, nil).
Once()
getUserInformationCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrDecodeRecord).
NotBefore(getUserInformationCall).
Once()
amocrmRepository.AssertNotCalled(t, "Insert")
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrDecodeRecord.Error())
assert.Equal(t, false, isLinked)
})
t.Run("Непредвиденная ошибка поиска пользователя amocrm при привязке аккаунта amocrm", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getAuthUserCall := authService.EXPECT().
GetAuthUserByToken(mock.Anything, authTokens.AccessToken).
Return(&authUser, nil).
Once()
getUserInformationCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
findByAmocrmIDCall := amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrNoRecord).
NotBefore(getUserInformationCall).
Once()
amocrmRepository.EXPECT().
Insert(mock.Anything, &models.AmocrmUser{
UserID: authUser.ID,
AmocrmID: strconv.Itoa(int(amocrmUserInformation.ID)),
Information: amocrmUserInformation,
}).
Return(nil, errors.ErrInsertRecord).
NotBefore(getAuthUserCall, findByAmocrmIDCall).
Once()
isLinked, err := amocrmService.Link(context.Background(), inputCode, authTokens.AccessToken)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInsertRecord.Error())
assert.Equal(t, false, isLinked)
})
}
func TestAmocrmAuth(t *testing.T) {
t.Run("Успешная авторизация аккаунта через amocrm (пользователь отсутствует в системе)", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
findByIDCall := amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrNoRecord).
NotBefore(getUserInfoByCodeCall).
Once()
registerCall := authService.EXPECT().
Register(mock.Anything, &models.AuthUserInformation{}).
Return(&authTokens, authUser.ID, nil).
NotBefore(findByIDCall).
Once()
amocrmRepository.EXPECT().
Insert(mock.Anything, &models.AmocrmUser{
UserID: authUser.ID,
AmocrmID: strconv.Itoa(int(amocrmUserInformation.ID)),
Information: amocrmUserInformation,
}).
Return(&amocrmUser, nil).
NotBefore(registerCall).
Once()
authService.AssertNotCalled(t, "Login")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.NoError(t, err)
assert.Equal(t, &authTokens, tokens)
})
t.Run("Успешная авторизация аккаунта через amocrm (пользователь присутствует в системе)", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
findByIDCall := amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(&amocrmUser, nil).
NotBefore(getUserInfoByCodeCall).
Once()
authService.EXPECT().
Login(mock.Anything, amocrmUser.UserID).
Return(&authTokens, nil).
NotBefore(findByIDCall).
Once()
authService.AssertNotCalled(t, "Register")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.NoError(t, err)
assert.Equal(t, &authTokens, tokens)
})
t.Run("Ошибка логина при авторизации через amocrm (пользователь присутствует в системе)", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(&amocrmUser, nil).
NotBefore(getUserInfoByCodeCall).
Once()
authService.EXPECT().
Login(mock.Anything, amocrmUser.UserID).
Return(nil, errors.ErrMethodNotImplemented).
Once()
authService.AssertNotCalled(t, "Register")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, tokens)
})
t.Run("Ошибка добавления пользователя amocrm в БД", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrNoRecord).
NotBefore(getUserInfoByCodeCall).
Once()
authService.EXPECT().
Register(mock.Anything, &models.AuthUserInformation{}).
Return(&authTokens, authUser.ID, nil).
Once()
amocrmRepository.EXPECT().
Insert(mock.Anything, &models.AmocrmUser{
UserID: authUser.ID,
AmocrmID: strconv.Itoa(int(amocrmUserInformation.ID)),
Information: amocrmUserInformation,
}).
Return(nil, errors.ErrInsertRecord).
Once()
authService.AssertNotCalled(t, "Login")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInsertRecord.Error())
assert.Nil(t, tokens)
})
t.Run("Ошибка регистрации пользователя amocrm", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrNoRecord).
NotBefore(getUserInfoByCodeCall).
Once()
authService.EXPECT().
Register(mock.Anything, &models.AuthUserInformation{}).
Return(nil, "", errors.ErrMethodNotImplemented).
Once()
amocrmRepository.AssertNotCalled(t, "Insert")
authService.AssertNotCalled(t, "Login")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, tokens)
})
t.Run("Ошибка получения пользователя amocrm при авторизации через аккаунт", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
getUserInfoByCodeCall := oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(&amocrmUserInformation, nil).
Once()
amocrmRepository.EXPECT().
FindByID(mock.Anything, strconv.Itoa(int(amocrmUserInformation.ID))).
Return(nil, errors.ErrFindRecord).
NotBefore(getUserInfoByCodeCall).
Once()
amocrmRepository.AssertNotCalled(t, "Insert")
authService.AssertNotCalled(t, "Login")
authService.AssertNotCalled(t, "Register")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrFindRecord.Error())
assert.Nil(t, tokens)
})
t.Run("Ошибка получения информации пользователя amocrm при авторизации через аккаунт", func(t *testing.T) {
oauthService := mocks.NewOauthService[models.AmocrmUserInformation](t)
authService := mocks.NewAuthService(t)
amocrmRepository := mocks.NewAmocrmRepository(t)
amocrmService := amocrm.New(&amocrm.Deps{
Logger: logrus.New(),
AuthService: authService,
OAuthService: oauthService,
Repository: amocrmRepository,
})
oauthService.EXPECT().
GetUserInformationByCode(mock.Anything, inputCode).
Return(nil, errors.ErrMethodNotImplemented).
Once()
amocrmRepository.AssertNotCalled(t, "Insert")
amocrmRepository.AssertNotCalled(t, "FindByID")
authService.AssertNotCalled(t, "Login")
authService.AssertNotCalled(t, "Register")
tokens, err := amocrmService.Auth(context.Background(), inputCode)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, tokens)
})
}

@ -0,0 +1,203 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
// AmocrmRepository is an autogenerated mock type for the amocrmRepository type
type AmocrmRepository struct {
mock.Mock
}
type AmocrmRepository_Expecter struct {
mock *mock.Mock
}
func (_m *AmocrmRepository) EXPECT() *AmocrmRepository_Expecter {
return &AmocrmRepository_Expecter{mock: &_m.Mock}
}
// FindByID provides a mock function with given fields: ctx, amocrmID
func (_m *AmocrmRepository) FindByID(ctx context.Context, amocrmID string) (*models.AmocrmUser, error) {
ret := _m.Called(ctx, amocrmID)
var r0 *models.AmocrmUser
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.AmocrmUser, error)); ok {
return rf(ctx, amocrmID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.AmocrmUser); ok {
r0 = rf(ctx, amocrmID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.AmocrmUser)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, amocrmID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AmocrmRepository_FindByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByID'
type AmocrmRepository_FindByID_Call struct {
*mock.Call
}
// FindByID is a helper method to define mock.On call
// - ctx context.Context
// - amocrmID string
func (_e *AmocrmRepository_Expecter) FindByID(ctx interface{}, amocrmID interface{}) *AmocrmRepository_FindByID_Call {
return &AmocrmRepository_FindByID_Call{Call: _e.mock.On("FindByID", ctx, amocrmID)}
}
func (_c *AmocrmRepository_FindByID_Call) Run(run func(ctx context.Context, amocrmID string)) *AmocrmRepository_FindByID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AmocrmRepository_FindByID_Call) Return(_a0 *models.AmocrmUser, _a1 error) *AmocrmRepository_FindByID_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AmocrmRepository_FindByID_Call) RunAndReturn(run func(context.Context, string) (*models.AmocrmUser, error)) *AmocrmRepository_FindByID_Call {
_c.Call.Return(run)
return _c
}
// FindByUserID provides a mock function with given fields: ctx, userID
func (_m *AmocrmRepository) FindByUserID(ctx context.Context, userID string) (*models.AmocrmUser, error) {
ret := _m.Called(ctx, userID)
var r0 *models.AmocrmUser
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.AmocrmUser, error)); ok {
return rf(ctx, userID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.AmocrmUser); ok {
r0 = rf(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.AmocrmUser)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AmocrmRepository_FindByUserID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByUserID'
type AmocrmRepository_FindByUserID_Call struct {
*mock.Call
}
// FindByUserID is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *AmocrmRepository_Expecter) FindByUserID(ctx interface{}, userID interface{}) *AmocrmRepository_FindByUserID_Call {
return &AmocrmRepository_FindByUserID_Call{Call: _e.mock.On("FindByUserID", ctx, userID)}
}
func (_c *AmocrmRepository_FindByUserID_Call) Run(run func(ctx context.Context, userID string)) *AmocrmRepository_FindByUserID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AmocrmRepository_FindByUserID_Call) Return(_a0 *models.AmocrmUser, _a1 error) *AmocrmRepository_FindByUserID_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AmocrmRepository_FindByUserID_Call) RunAndReturn(run func(context.Context, string) (*models.AmocrmUser, error)) *AmocrmRepository_FindByUserID_Call {
_c.Call.Return(run)
return _c
}
// Insert provides a mock function with given fields: ctx, user
func (_m *AmocrmRepository) Insert(ctx context.Context, user *models.AmocrmUser) (*models.AmocrmUser, error) {
ret := _m.Called(ctx, user)
var r0 *models.AmocrmUser
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *models.AmocrmUser) (*models.AmocrmUser, error)); ok {
return rf(ctx, user)
}
if rf, ok := ret.Get(0).(func(context.Context, *models.AmocrmUser) *models.AmocrmUser); ok {
r0 = rf(ctx, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.AmocrmUser)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *models.AmocrmUser) error); ok {
r1 = rf(ctx, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AmocrmRepository_Insert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Insert'
type AmocrmRepository_Insert_Call struct {
*mock.Call
}
// Insert is a helper method to define mock.On call
// - ctx context.Context
// - user *models.AmocrmUser
func (_e *AmocrmRepository_Expecter) Insert(ctx interface{}, user interface{}) *AmocrmRepository_Insert_Call {
return &AmocrmRepository_Insert_Call{Call: _e.mock.On("Insert", ctx, user)}
}
func (_c *AmocrmRepository_Insert_Call) Run(run func(ctx context.Context, user *models.AmocrmUser)) *AmocrmRepository_Insert_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*models.AmocrmUser))
})
return _c
}
func (_c *AmocrmRepository_Insert_Call) Return(_a0 *models.AmocrmUser, _a1 error) *AmocrmRepository_Insert_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AmocrmRepository_Insert_Call) RunAndReturn(run func(context.Context, *models.AmocrmUser) (*models.AmocrmUser, error)) *AmocrmRepository_Insert_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewAmocrmRepository interface {
mock.TestingT
Cleanup(func())
}
// NewAmocrmRepository creates a new instance of AmocrmRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewAmocrmRepository(t mockConstructorTestingTNewAmocrmRepository) *AmocrmRepository {
mock := &AmocrmRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,210 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
// AuthService is an autogenerated mock type for the authService type
type AuthService struct {
mock.Mock
}
type AuthService_Expecter struct {
mock *mock.Mock
}
func (_m *AuthService) EXPECT() *AuthService_Expecter {
return &AuthService_Expecter{mock: &_m.Mock}
}
// GetAuthUserByToken provides a mock function with given fields: ctx, accessToken
func (_m *AuthService) GetAuthUserByToken(ctx context.Context, accessToken string) (*models.AuthUser, error) {
ret := _m.Called(ctx, accessToken)
var r0 *models.AuthUser
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.AuthUser, error)); ok {
return rf(ctx, accessToken)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.AuthUser); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.AuthUser)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, accessToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AuthService_GetAuthUserByToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthUserByToken'
type AuthService_GetAuthUserByToken_Call struct {
*mock.Call
}
// GetAuthUserByToken is a helper method to define mock.On call
// - ctx context.Context
// - accessToken string
func (_e *AuthService_Expecter) GetAuthUserByToken(ctx interface{}, accessToken interface{}) *AuthService_GetAuthUserByToken_Call {
return &AuthService_GetAuthUserByToken_Call{Call: _e.mock.On("GetAuthUserByToken", ctx, accessToken)}
}
func (_c *AuthService_GetAuthUserByToken_Call) Run(run func(ctx context.Context, accessToken string)) *AuthService_GetAuthUserByToken_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AuthService_GetAuthUserByToken_Call) Return(_a0 *models.AuthUser, _a1 error) *AuthService_GetAuthUserByToken_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AuthService_GetAuthUserByToken_Call) RunAndReturn(run func(context.Context, string) (*models.AuthUser, error)) *AuthService_GetAuthUserByToken_Call {
_c.Call.Return(run)
return _c
}
// Login provides a mock function with given fields: ctx, userID
func (_m *AuthService) Login(ctx context.Context, userID string) (*models.Tokens, error) {
ret := _m.Called(ctx, userID)
var r0 *models.Tokens
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.Tokens, error)); ok {
return rf(ctx, userID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.Tokens); ok {
r0 = rf(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tokens)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AuthService_Login_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Login'
type AuthService_Login_Call struct {
*mock.Call
}
// Login is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *AuthService_Expecter) Login(ctx interface{}, userID interface{}) *AuthService_Login_Call {
return &AuthService_Login_Call{Call: _e.mock.On("Login", ctx, userID)}
}
func (_c *AuthService_Login_Call) Run(run func(ctx context.Context, userID string)) *AuthService_Login_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AuthService_Login_Call) Return(_a0 *models.Tokens, _a1 error) *AuthService_Login_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AuthService_Login_Call) RunAndReturn(run func(context.Context, string) (*models.Tokens, error)) *AuthService_Login_Call {
_c.Call.Return(run)
return _c
}
// Register provides a mock function with given fields: ctx, information
func (_m *AuthService) Register(ctx context.Context, information *models.AuthUserInformation) (*models.Tokens, string, error) {
ret := _m.Called(ctx, information)
var r0 *models.Tokens
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, *models.AuthUserInformation) (*models.Tokens, string, error)); ok {
return rf(ctx, information)
}
if rf, ok := ret.Get(0).(func(context.Context, *models.AuthUserInformation) *models.Tokens); ok {
r0 = rf(ctx, information)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tokens)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *models.AuthUserInformation) string); ok {
r1 = rf(ctx, information)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(context.Context, *models.AuthUserInformation) error); ok {
r2 = rf(ctx, information)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// AuthService_Register_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Register'
type AuthService_Register_Call struct {
*mock.Call
}
// Register is a helper method to define mock.On call
// - ctx context.Context
// - information *models.AuthUserInformation
func (_e *AuthService_Expecter) Register(ctx interface{}, information interface{}) *AuthService_Register_Call {
return &AuthService_Register_Call{Call: _e.mock.On("Register", ctx, information)}
}
func (_c *AuthService_Register_Call) Run(run func(ctx context.Context, information *models.AuthUserInformation)) *AuthService_Register_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*models.AuthUserInformation))
})
return _c
}
func (_c *AuthService_Register_Call) Return(_a0 *models.Tokens, _a1 string, _a2 error) *AuthService_Register_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *AuthService_Register_Call) RunAndReturn(run func(context.Context, *models.AuthUserInformation) (*models.Tokens, string, error)) *AuthService_Register_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewAuthService interface {
mock.TestingT
Cleanup(func())
}
// NewAuthService creates a new instance of AuthService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewAuthService(t mockConstructorTestingTNewAuthService) *AuthService {
mock := &AuthService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,92 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// OauthService is an autogenerated mock type for the oauthService type
type OauthService[T interface{}] struct {
mock.Mock
}
type OauthService_Expecter[T interface{}] struct {
mock *mock.Mock
}
func (_m *OauthService[T]) EXPECT() *OauthService_Expecter[T] {
return &OauthService_Expecter[T]{mock: &_m.Mock}
}
// GetUserInformationByCode provides a mock function with given fields: ctx, code
func (_m *OauthService[T]) GetUserInformationByCode(ctx context.Context, code string) (*T, error) {
ret := _m.Called(ctx, code)
var r0 *T
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*T, error)); ok {
return rf(ctx, code)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *T); ok {
r0 = rf(ctx, code)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*T)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, code)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OauthService_GetUserInformationByCode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserInformationByCode'
type OauthService_GetUserInformationByCode_Call[T interface{}] struct {
*mock.Call
}
// GetUserInformationByCode is a helper method to define mock.On call
// - ctx context.Context
// - code string
func (_e *OauthService_Expecter[T]) GetUserInformationByCode(ctx interface{}, code interface{}) *OauthService_GetUserInformationByCode_Call[T] {
return &OauthService_GetUserInformationByCode_Call[T]{Call: _e.mock.On("GetUserInformationByCode", ctx, code)}
}
func (_c *OauthService_GetUserInformationByCode_Call[T]) Run(run func(ctx context.Context, code string)) *OauthService_GetUserInformationByCode_Call[T] {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *OauthService_GetUserInformationByCode_Call[T]) Return(_a0 *T, _a1 error) *OauthService_GetUserInformationByCode_Call[T] {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *OauthService_GetUserInformationByCode_Call[T]) RunAndReturn(run func(context.Context, string) (*T, error)) *OauthService_GetUserInformationByCode_Call[T] {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewOauthService interface {
mock.TestingT
Cleanup(func())
}
// NewOauthService creates a new instance of OauthService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewOauthService[T interface{}](t mockConstructorTestingTNewOauthService) *OauthService[T] {
mock := &OauthService[T]{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,97 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
//go:generate mockery --name authClient
type authClient interface {
GetUser(ctx context.Context, userID string) (*models.AuthUser, error)
Register(ctx context.Context, request *models.RegisterRequest) (*models.Tokens, error)
Exchange(ctx context.Context, userID, signature string) (*models.Tokens, error)
}
//go:generate mockery --name encryptionService
type encryptionService interface {
VerifyJWT(token string) (string, error)
SignCommonSecret() ([]byte, error)
}
type Deps struct {
Logger *logrus.Logger
AuthClient authClient
EncryptionService encryptionService
}
type Service struct {
logger *logrus.Logger
authClient authClient
encryptionService encryptionService
}
func New(deps *Deps) *Service {
return &Service{
logger: deps.Logger,
authClient: deps.AuthClient,
encryptionService: deps.EncryptionService,
}
}
func (receiver *Service) Register(ctx context.Context, information *models.AuthUserInformation) (*models.Tokens, string, error) {
tokens, err := receiver.authClient.Register(ctx, &models.RegisterRequest{
Login: fmt.Sprintf("user_%d", time.Now().UnixNano()),
PhoneNumber: information.PhoneNumber,
Email: information.Email,
Password: utils.GetRandomString(14),
})
if err != nil {
receiver.logger.Errorf("failed to register user on <Register> of <AuthService>: %v", err)
return nil, "", err
}
userID, err := receiver.encryptionService.VerifyJWT(tokens.AccessToken)
if err != nil {
receiver.logger.Errorf("failed to verify jwt on <Register> of <AuthService>: %v", err)
return nil, "", err
}
return tokens, userID, nil
}
func (receiver *Service) Login(ctx context.Context, userID string) (*models.Tokens, error) {
signature, err := receiver.encryptionService.SignCommonSecret()
if err != nil {
receiver.logger.Errorf("failed to sign common secret on <Login> of <AuthService>: %v", err)
return nil, err
}
tokens, err := receiver.authClient.Exchange(ctx, userID, string(signature))
if err != nil {
receiver.logger.Errorf("failed to exchange code on <Login> of <AuthService>: %v", err)
return nil, err
}
return tokens, nil
}
func (receiver *Service) GetAuthUserByToken(ctx context.Context, accessToken string) (*models.AuthUser, error) {
userID, err := receiver.encryptionService.VerifyJWT(accessToken)
if err != nil {
receiver.logger.Errorf("failed to vetify jwt on <GetAuthUserByToken> of <AuthService>: %v", err)
return nil, err
}
user, err := receiver.authClient.GetUser(ctx, userID)
if err != nil {
receiver.logger.Errorf("failed to get user on <GetAuthUserByToken> of <AuthService>: %v", err)
return nil, err
}
return user, nil
}

@ -0,0 +1,288 @@
package auth_test
import (
"context"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/auth"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/auth/mocks"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/validate"
)
var (
userID = "user-id-14"
authUserInformation = models.AuthUserInformation{
PhoneNumber: "+75819369631",
Email: "test@mail.ru",
}
authTokens = models.Tokens{
AccessToken: "access-token-auth",
RefreshToken: "refresh-token-auth",
}
authUser = models.AuthUser{
ID: "1geat",
Login: "gkohoeh",
PhoneNumber: authUserInformation.PhoneNumber,
Email: authUserInformation.Email,
}
validateRegisterRequest = mock.MatchedBy(func(request *models.RegisterRequest) bool {
isLoginFilled := !validate.IsStringEmpty(request.Login)
isPasswordFilled := !validate.IsStringEmpty(request.Password)
isEmailFilled := request.Email == authUserInformation.Email
isPhoneFilled := request.PhoneNumber == authUserInformation.PhoneNumber
if isLoginFilled && isPasswordFilled && isEmailFilled && isPhoneFilled {
return true
}
return false
})
)
func TestAuthRegister(t *testing.T) {
t.Run("Успешная регистарция пользователя", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
registerAuthUserCall := authClient.EXPECT().
Register(mock.Anything, validateRegisterRequest).
Return(&authTokens, nil).
Once()
encryptionService.EXPECT().
VerifyJWT(authTokens.AccessToken).
Return(userID, nil).
NotBefore(registerAuthUserCall).
Once()
registrationTokens, registeredUserID, err := authService.Register(context.Background(), &authUserInformation)
assert.NoError(t, err)
assert.Equal(t, &authTokens, registrationTokens)
assert.Equal(t, userID, registeredUserID)
})
t.Run("Ошибка регистрации пользователя в auth микросервисе при регистрации", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
authClient.EXPECT().
Register(mock.Anything, validateRegisterRequest).
Return(nil, errors.ErrMethodNotImplemented).
Once()
encryptionService.AssertNotCalled(t, "VerifyJWT")
registrationTokens, registeredUserID, err := authService.Register(context.Background(), &authUserInformation)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, registrationTokens)
assert.Empty(t, registeredUserID)
})
t.Run("Ошибка подтверждения токена единой системы авторизации при регистрации", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
registerAuthUserCall := authClient.EXPECT().
Register(mock.Anything, validateRegisterRequest).
Return(&authTokens, nil).
Once()
encryptionService.EXPECT().
VerifyJWT(authTokens.AccessToken).
Return("", errors.ErrInvalidArgs).
NotBefore(registerAuthUserCall).
Once()
registrationTokens, registeredUserID, err := authService.Register(context.Background(), &authUserInformation)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInvalidArgs.Error())
assert.Nil(t, registrationTokens)
assert.Empty(t, registeredUserID)
})
}
func TestAuthLogin(t *testing.T) {
t.Run("Успешная авторизация пользователя", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptCodeCall := encryptionService.EXPECT().
SignCommonSecret().
Return([]byte{116, 116}, nil).
Once()
authClient.EXPECT().
Exchange(mock.Anything, userID, "tt").
Return(&authTokens, nil).
NotBefore(encryptCodeCall).
Once()
loginTokens, err := authService.Login(context.Background(), userID)
assert.NoError(t, err)
assert.Equal(t, &authTokens, loginTokens)
})
t.Run("Ошибка шифрования кода обмена при авторизации", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptionService.EXPECT().
SignCommonSecret().
Return([]byte{111, 116}, errors.ErrInvalidArgs).
Once()
authClient.AssertNotCalled(t, "Exchange")
loginTokens, err := authService.Login(context.Background(), userID)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrInvalidArgs.Error())
assert.Nil(t, loginTokens)
})
t.Run("Ошибка обмена зашифрованного кода на токены при авторизации", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptCodeCall := encryptionService.EXPECT().
SignCommonSecret().
Return([]byte{97, 97}, nil).
Once()
authClient.EXPECT().
Exchange(mock.Anything, userID, "aa").
Return(nil, errors.ErrMethodNotImplemented).
NotBefore(encryptCodeCall).
Once()
loginTokens, err := authService.Login(context.Background(), userID)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, loginTokens)
})
}
func TestGetAuthUserByToken(t *testing.T) {
t.Run("Успешное получение информации о пользователе ЕСА через токен", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptCodeCall := encryptionService.EXPECT().
VerifyJWT(authTokens.AccessToken).
Return(userID, nil).
Once()
authClient.EXPECT().
GetUser(mock.Anything, userID).
Return(&authUser, nil).
NotBefore(encryptCodeCall).
Once()
user, err := authService.GetAuthUserByToken(context.Background(), authTokens.AccessToken)
assert.NoError(t, err)
assert.Equal(t, &authUser, user)
})
t.Run("Ошибка запроса получения пользователя ЕСА", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptCodeCall := encryptionService.EXPECT().
VerifyJWT(authTokens.AccessToken).
Return(userID, nil).
Once()
authClient.EXPECT().
GetUser(mock.Anything, userID).
Return(&authUser, errors.ErrMethodNotImplemented).
NotBefore(encryptCodeCall).
Once()
user, err := authService.GetAuthUserByToken(context.Background(), authTokens.AccessToken)
assert.Error(t, err)
assert.NotNil(t, err)
assert.EqualError(t, err, errors.ErrMethodNotImplemented.Error())
assert.Nil(t, user)
})
t.Run("Ошибка подтверждения токена при получении пользователя ЕСА по токену", func(t *testing.T) {
authClient := mocks.NewAuthClient(t)
encryptionService := mocks.NewEncryptionService(t)
authService := auth.New(&auth.Deps{
Logger: logrus.New(),
AuthClient: authClient,
EncryptionService: encryptionService,
})
encryptionService.EXPECT().
VerifyJWT(authTokens.AccessToken).
Return("", errors.ErrEmptyArgs).
Once()
authClient.AssertNotCalled(t, "GetUser")
user, err := authService.GetAuthUserByToken(context.Background(), authTokens.AccessToken)
assert.Error(t, err)
assert.NotNil(t, err)
assert.EqualError(t, err, errors.ErrEmptyArgs.Error())
assert.Nil(t, user)
})
}

@ -0,0 +1,204 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
// AuthClient is an autogenerated mock type for the authClient type
type AuthClient struct {
mock.Mock
}
type AuthClient_Expecter struct {
mock *mock.Mock
}
func (_m *AuthClient) EXPECT() *AuthClient_Expecter {
return &AuthClient_Expecter{mock: &_m.Mock}
}
// Exchange provides a mock function with given fields: ctx, userID, signature
func (_m *AuthClient) Exchange(ctx context.Context, userID string, signature string) (*models.Tokens, error) {
ret := _m.Called(ctx, userID, signature)
var r0 *models.Tokens
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*models.Tokens, error)); ok {
return rf(ctx, userID, signature)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) *models.Tokens); ok {
r0 = rf(ctx, userID, signature)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tokens)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, userID, signature)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AuthClient_Exchange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exchange'
type AuthClient_Exchange_Call struct {
*mock.Call
}
// Exchange is a helper method to define mock.On call
// - ctx context.Context
// - userID string
// - signature string
func (_e *AuthClient_Expecter) Exchange(ctx interface{}, userID interface{}, signature interface{}) *AuthClient_Exchange_Call {
return &AuthClient_Exchange_Call{Call: _e.mock.On("Exchange", ctx, userID, signature)}
}
func (_c *AuthClient_Exchange_Call) Run(run func(ctx context.Context, userID string, signature string)) *AuthClient_Exchange_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *AuthClient_Exchange_Call) Return(_a0 *models.Tokens, _a1 error) *AuthClient_Exchange_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AuthClient_Exchange_Call) RunAndReturn(run func(context.Context, string, string) (*models.Tokens, error)) *AuthClient_Exchange_Call {
_c.Call.Return(run)
return _c
}
// GetUser provides a mock function with given fields: ctx, userID
func (_m *AuthClient) GetUser(ctx context.Context, userID string) (*models.AuthUser, error) {
ret := _m.Called(ctx, userID)
var r0 *models.AuthUser
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*models.AuthUser, error)); ok {
return rf(ctx, userID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *models.AuthUser); ok {
r0 = rf(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.AuthUser)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AuthClient_GetUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUser'
type AuthClient_GetUser_Call struct {
*mock.Call
}
// GetUser is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *AuthClient_Expecter) GetUser(ctx interface{}, userID interface{}) *AuthClient_GetUser_Call {
return &AuthClient_GetUser_Call{Call: _e.mock.On("GetUser", ctx, userID)}
}
func (_c *AuthClient_GetUser_Call) Run(run func(ctx context.Context, userID string)) *AuthClient_GetUser_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *AuthClient_GetUser_Call) Return(_a0 *models.AuthUser, _a1 error) *AuthClient_GetUser_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AuthClient_GetUser_Call) RunAndReturn(run func(context.Context, string) (*models.AuthUser, error)) *AuthClient_GetUser_Call {
_c.Call.Return(run)
return _c
}
// Register provides a mock function with given fields: ctx, request
func (_m *AuthClient) Register(ctx context.Context, request *models.RegisterRequest) (*models.Tokens, error) {
ret := _m.Called(ctx, request)
var r0 *models.Tokens
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *models.RegisterRequest) (*models.Tokens, error)); ok {
return rf(ctx, request)
}
if rf, ok := ret.Get(0).(func(context.Context, *models.RegisterRequest) *models.Tokens); ok {
r0 = rf(ctx, request)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tokens)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *models.RegisterRequest) error); ok {
r1 = rf(ctx, request)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AuthClient_Register_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Register'
type AuthClient_Register_Call struct {
*mock.Call
}
// Register is a helper method to define mock.On call
// - ctx context.Context
// - request *models.RegisterRequest
func (_e *AuthClient_Expecter) Register(ctx interface{}, request interface{}) *AuthClient_Register_Call {
return &AuthClient_Register_Call{Call: _e.mock.On("Register", ctx, request)}
}
func (_c *AuthClient_Register_Call) Run(run func(ctx context.Context, request *models.RegisterRequest)) *AuthClient_Register_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*models.RegisterRequest))
})
return _c
}
func (_c *AuthClient_Register_Call) Return(_a0 *models.Tokens, _a1 error) *AuthClient_Register_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AuthClient_Register_Call) RunAndReturn(run func(context.Context, *models.RegisterRequest) (*models.Tokens, error)) *AuthClient_Register_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewAuthClient interface {
mock.TestingT
Cleanup(func())
}
// NewAuthClient creates a new instance of AuthClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewAuthClient(t mockConstructorTestingTNewAuthClient) *AuthClient {
mock := &AuthClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,138 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// EncryptionService is an autogenerated mock type for the encryptionService type
type EncryptionService struct {
mock.Mock
}
type EncryptionService_Expecter struct {
mock *mock.Mock
}
func (_m *EncryptionService) EXPECT() *EncryptionService_Expecter {
return &EncryptionService_Expecter{mock: &_m.Mock}
}
// SignCommonSecret provides a mock function with given fields:
func (_m *EncryptionService) SignCommonSecret() ([]byte, error) {
ret := _m.Called()
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []byte); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// EncryptionService_SignCommonSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SignCommonSecret'
type EncryptionService_SignCommonSecret_Call struct {
*mock.Call
}
// SignCommonSecret is a helper method to define mock.On call
func (_e *EncryptionService_Expecter) SignCommonSecret() *EncryptionService_SignCommonSecret_Call {
return &EncryptionService_SignCommonSecret_Call{Call: _e.mock.On("SignCommonSecret")}
}
func (_c *EncryptionService_SignCommonSecret_Call) Run(run func()) *EncryptionService_SignCommonSecret_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *EncryptionService_SignCommonSecret_Call) Return(_a0 []byte, _a1 error) *EncryptionService_SignCommonSecret_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *EncryptionService_SignCommonSecret_Call) RunAndReturn(run func() ([]byte, error)) *EncryptionService_SignCommonSecret_Call {
_c.Call.Return(run)
return _c
}
// VerifyJWT provides a mock function with given fields: token
func (_m *EncryptionService) VerifyJWT(token string) (string, error) {
ret := _m.Called(token)
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(token)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(token)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// EncryptionService_VerifyJWT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyJWT'
type EncryptionService_VerifyJWT_Call struct {
*mock.Call
}
// VerifyJWT is a helper method to define mock.On call
// - token string
func (_e *EncryptionService_Expecter) VerifyJWT(token interface{}) *EncryptionService_VerifyJWT_Call {
return &EncryptionService_VerifyJWT_Call{Call: _e.mock.On("VerifyJWT", token)}
}
func (_c *EncryptionService_VerifyJWT_Call) Run(run func(token string)) *EncryptionService_VerifyJWT_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *EncryptionService_VerifyJWT_Call) Return(_a0 string, _a1 error) *EncryptionService_VerifyJWT_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *EncryptionService_VerifyJWT_Call) RunAndReturn(run func(string) (string, error)) *EncryptionService_VerifyJWT_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewEncryptionService interface {
mock.TestingT
Cleanup(func())
}
// NewEncryptionService creates a new instance of EncryptionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewEncryptionService(t mockConstructorTestingTNewEncryptionService) *EncryptionService {
mock := &EncryptionService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,103 @@
package encrypt
import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"fmt"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
//go:generate mockery --name jwtUtil
type jwtUtil interface {
Validate(string) (*models.JWTAuthUser, error)
}
type ServiceDeps struct {
JWT jwtUtil
/* Публичный ключ для верификации подписи кривой Эдвардса (Edwards curve) */
PublicCurveKey string
/* Приватный ключ для верификации подписи кривой Эдвардса (Edwards curve) */
PrivateCurveKey string
/*
Обший секретный знаменатель для шифрования и верификации подписи
(должен быть одинаковым для всех микросервисов)
*/
SignSecret string
}
type Service struct {
jwt jwtUtil
publicCurveKey string
privateCurveKey string
signSecret string
}
func New(deps *ServiceDeps) *Service {
return &Service{
jwt: deps.JWT,
publicCurveKey: deps.PublicCurveKey,
privateCurveKey: deps.PrivateCurveKey,
signSecret: deps.SignSecret,
}
}
func (receiver *Service) VerifySignature(signature []byte) (isValid bool, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered sign error on <VerifySignature> of <EncryptService>: %v", recovered)
}
}()
block, _ := pem.Decode([]byte(receiver.publicCurveKey))
if block == nil {
return false, fmt.Errorf("public key block is nil")
}
rawPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return false, fmt.Errorf("failed parse public key on <VerifySignature> of <EncryptService>: %w", err)
}
publicKey, ok := rawPublicKey.(ed25519.PublicKey)
if !ok {
return false, fmt.Errorf("failed convert to ed25519.PrivateKey on <VerifySignature> of <EncryptService>: %w", err)
}
return ed25519.Verify(publicKey, []byte(receiver.signSecret), signature), nil
}
func (receiver *Service) SignCommonSecret() (signature []byte, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered sign error on <SignCommonSecret> of <EncryptService>: %v", recovered)
}
}()
block, _ := pem.Decode([]byte(receiver.privateCurveKey))
rawPrivateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return []byte{}, fmt.Errorf("failed parse private key on <SignCommonSecret> of <EncryptService>: %w", err)
}
privateKey, ok := rawPrivateKey.(ed25519.PrivateKey)
if !ok {
return []byte{}, fmt.Errorf("failed convert to ed25519.PrivateKey on <SignCommonSecret> of <EncryptService>: %w", err)
}
return ed25519.Sign(privateKey, []byte(receiver.signSecret)), nil
}
func (receiver *Service) VerifyJWT(token string) (string, error) {
validatedJwtPayload, err := receiver.jwt.Validate(token)
if err != nil {
return "", fmt.Errorf("failed to verify jwt on <VerifyJWT> of <EncryptService>: %w", err)
}
return validatedJwtPayload.ID, nil
}

@ -0,0 +1,182 @@
package encrypt_test
import (
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/encrypt"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/encrypt/mocks"
)
var (
privateKeyCurve25519 = strings.Replace(
`-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs
-----END PRIVATE KEY-----`,
"\t",
"",
-1,
)
privateKeyCurve25519Invalid = strings.Replace(
`-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIE3vZvODgWAnUIhd8de5oZhY48gc23EWfrfs
-----END PRIVATE KEY-----`,
"\t",
"",
-1,
)
publicKeyCurve25519 = strings.Replace(
`-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAEbnIvjIMle4rqVol6K2XUqOxHy1KJoNoZdKJrRUPKL4=
-----END PUBLIC KEY-----`,
"\t",
"",
-1,
)
publicKeyCurve25519InvalidLength = strings.Replace(
`-----BEGIN PUBLIC KEY-----
MowBQYDK2VwA9yEAEbnIvjIMle4rqVol6K2XUqOxHy1KJoNoZdKJrRUPKL4=
-----END PUBLIC KEY-----`,
"\t",
"",
-1,
)
)
func TestSignCommonSecret(t *testing.T) {
t.Run("Успешная подпись общего секрета", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PrivateCurveKey: privateKeyCurve25519,
SignSecret: "secret",
})
assert.NotPanics(t, func() {
encryptedText, err := encryptService.SignCommonSecret()
assert.NoError(t, err)
assert.NotEmpty(t, encryptedText)
assert.NotZero(t, encryptedText)
})
})
t.Run("Ошибка подписи из-за кривого ключа (заголовок имеется)", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PrivateCurveKey: privateKeyCurve25519Invalid,
SignSecret: "secret",
})
assert.NotPanics(t, func() {
encryptedText, err := encryptService.SignCommonSecret()
assert.Error(t, err)
assert.Empty(t, encryptedText)
assert.Zero(t, encryptedText)
})
})
t.Run("Ошибка подписи из-за рандомного кривого ключа", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PrivateCurveKey: "testtesttesttest",
SignSecret: "secret",
})
assert.NotPanics(t, func() {
encryptedText, err := encryptService.SignCommonSecret()
assert.Error(t, err)
assert.Empty(t, encryptedText)
assert.Zero(t, encryptedText)
})
})
}
func TestVerifySignature(t *testing.T) {
t.Run("Успешное подтвеждение подписи", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PublicCurveKey: publicKeyCurve25519,
PrivateCurveKey: privateKeyCurve25519,
SignSecret: "secret",
})
assert.NotPanics(t, func() {
signature, _ := encryptService.SignCommonSecret()
isValid, err := encryptService.VerifySignature(signature)
assert.NoError(t, err)
assert.Equal(t, true, isValid)
})
})
t.Run("Неудачное подтверждение подписи из-за невалидности ключа", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PublicCurveKey: "teettaegarehah",
PrivateCurveKey: privateKeyCurve25519,
SignSecret: "secret",
})
assert.NotPanics(t, func() {
signature, _ := encryptService.SignCommonSecret()
isValid, err := encryptService.VerifySignature(signature)
assert.Error(t, err)
assert.Equal(t, false, isValid)
})
})
t.Run("Неудачное подтверждение подписи при использовании ключа у которого невалидный размер (слишком большой)", func(t *testing.T) {
encryptService := encrypt.New(&encrypt.ServiceDeps{
PublicCurveKey: publicKeyCurve25519InvalidLength,
PrivateCurveKey: privateKeyCurve25519,
SignSecret: "secret",
})
assert.NotPanics(t, func() {
signature, _ := encryptService.SignCommonSecret()
isValid, err := encryptService.VerifySignature(signature)
assert.Error(t, err)
assert.Equal(t, false, isValid)
})
})
}
func TestVerifyJWT(t *testing.T) {
jwtToken := "token-token"
jwtUser := models.JWTAuthUser{
ID: "id1",
}
t.Run("Успешное подтверждение токена", func(t *testing.T) {
jwtUtil := mocks.NewJwtUtil(t)
encryptService := encrypt.New(&encrypt.ServiceDeps{
JWT: jwtUtil,
})
jwtUtil.EXPECT().Validate(jwtToken).Return(&jwtUser, nil).Once()
id, err := encryptService.VerifyJWT(jwtToken)
assert.NoError(t, err)
assert.Equal(t, jwtUser.ID, id)
})
t.Run("Ошибка подтверждения токена", func(t *testing.T) {
jwtUtil := mocks.NewJwtUtil(t)
encryptService := encrypt.New(&encrypt.ServiceDeps{
JWT: jwtUtil,
})
jwtUtil.EXPECT().Validate(jwtToken).Return(nil, errors.New("validate jwt error")).Once()
id, err := encryptService.VerifyJWT(jwtToken)
assert.Error(t, err)
assert.Empty(t, id)
})
}

@ -0,0 +1,90 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
models "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
// JwtUtil is an autogenerated mock type for the jwtUtil type
type JwtUtil struct {
mock.Mock
}
type JwtUtil_Expecter struct {
mock *mock.Mock
}
func (_m *JwtUtil) EXPECT() *JwtUtil_Expecter {
return &JwtUtil_Expecter{mock: &_m.Mock}
}
// Validate provides a mock function with given fields: _a0
func (_m *JwtUtil) Validate(_a0 string) (*models.JWTAuthUser, error) {
ret := _m.Called(_a0)
var r0 *models.JWTAuthUser
var r1 error
if rf, ok := ret.Get(0).(func(string) (*models.JWTAuthUser, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(string) *models.JWTAuthUser); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.JWTAuthUser)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// JwtUtil_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
type JwtUtil_Validate_Call struct {
*mock.Call
}
// Validate is a helper method to define mock.On call
// - _a0 string
func (_e *JwtUtil_Expecter) Validate(_a0 interface{}) *JwtUtil_Validate_Call {
return &JwtUtil_Validate_Call{Call: _e.mock.On("Validate", _a0)}
}
func (_c *JwtUtil_Validate_Call) Run(run func(_a0 string)) *JwtUtil_Validate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *JwtUtil_Validate_Call) Return(_a0 *models.JWTAuthUser, _a1 error) *JwtUtil_Validate_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *JwtUtil_Validate_Call) RunAndReturn(run func(string) (*models.JWTAuthUser, error)) *JwtUtil_Validate_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewJwtUtil interface {
mock.TestingT
Cleanup(func())
}
// NewJwtUtil creates a new instance of JwtUtil. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewJwtUtil(t mockConstructorTestingTNewJwtUtil) *JwtUtil {
mock := &JwtUtil{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,166 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
oauth2 "golang.org/x/oauth2"
)
// OauthClient is an autogenerated mock type for the oauthClient type
type OauthClient struct {
mock.Mock
}
type OauthClient_Expecter struct {
mock *mock.Mock
}
func (_m *OauthClient) EXPECT() *OauthClient_Expecter {
return &OauthClient_Expecter{mock: &_m.Mock}
}
// AuthCodeURL provides a mock function with given fields: state, opts
func (_m *OauthClient) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, state)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 string
if rf, ok := ret.Get(0).(func(string, ...oauth2.AuthCodeOption) string); ok {
r0 = rf(state, opts...)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// OauthClient_AuthCodeURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AuthCodeURL'
type OauthClient_AuthCodeURL_Call struct {
*mock.Call
}
// AuthCodeURL is a helper method to define mock.On call
// - state string
// - opts ...oauth2.AuthCodeOption
func (_e *OauthClient_Expecter) AuthCodeURL(state interface{}, opts ...interface{}) *OauthClient_AuthCodeURL_Call {
return &OauthClient_AuthCodeURL_Call{Call: _e.mock.On("AuthCodeURL",
append([]interface{}{state}, opts...)...)}
}
func (_c *OauthClient_AuthCodeURL_Call) Run(run func(state string, opts ...oauth2.AuthCodeOption)) *OauthClient_AuthCodeURL_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]oauth2.AuthCodeOption, len(args)-1)
for i, a := range args[1:] {
if a != nil {
variadicArgs[i] = a.(oauth2.AuthCodeOption)
}
}
run(args[0].(string), variadicArgs...)
})
return _c
}
func (_c *OauthClient_AuthCodeURL_Call) Return(_a0 string) *OauthClient_AuthCodeURL_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *OauthClient_AuthCodeURL_Call) RunAndReturn(run func(string, ...oauth2.AuthCodeOption) string) *OauthClient_AuthCodeURL_Call {
_c.Call.Return(run)
return _c
}
// Exchange provides a mock function with given fields: ctx, code, opts
func (_m *OauthClient) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, code)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *oauth2.Token
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)); ok {
return rf(ctx, code, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) *oauth2.Token); ok {
r0 = rf(ctx, code, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*oauth2.Token)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, ...oauth2.AuthCodeOption) error); ok {
r1 = rf(ctx, code, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OauthClient_Exchange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exchange'
type OauthClient_Exchange_Call struct {
*mock.Call
}
// Exchange is a helper method to define mock.On call
// - ctx context.Context
// - code string
// - opts ...oauth2.AuthCodeOption
func (_e *OauthClient_Expecter) Exchange(ctx interface{}, code interface{}, opts ...interface{}) *OauthClient_Exchange_Call {
return &OauthClient_Exchange_Call{Call: _e.mock.On("Exchange",
append([]interface{}{ctx, code}, opts...)...)}
}
func (_c *OauthClient_Exchange_Call) Run(run func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption)) *OauthClient_Exchange_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]oauth2.AuthCodeOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(oauth2.AuthCodeOption)
}
}
run(args[0].(context.Context), args[1].(string), variadicArgs...)
})
return _c
}
func (_c *OauthClient_Exchange_Call) Return(_a0 *oauth2.Token, _a1 error) *OauthClient_Exchange_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *OauthClient_Exchange_Call) RunAndReturn(run func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)) *OauthClient_Exchange_Call {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewOauthClient interface {
mock.TestingT
Cleanup(func())
}
// NewOauthClient creates a new instance of OauthClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewOauthClient(t mockConstructorTestingTNewOauthClient) *OauthClient {
mock := &OauthClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,92 @@
// Code generated by mockery v2.26.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// ServiceClient is an autogenerated mock type for the serviceClient type
type ServiceClient[T interface{}] struct {
mock.Mock
}
type ServiceClient_Expecter[T interface{}] struct {
mock *mock.Mock
}
func (_m *ServiceClient[T]) EXPECT() *ServiceClient_Expecter[T] {
return &ServiceClient_Expecter[T]{mock: &_m.Mock}
}
// GetUserInformation provides a mock function with given fields: ctx, accessToken
func (_m *ServiceClient[T]) GetUserInformation(ctx context.Context, accessToken string) (*T, error) {
ret := _m.Called(ctx, accessToken)
var r0 *T
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*T, error)); ok {
return rf(ctx, accessToken)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *T); ok {
r0 = rf(ctx, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*T)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, accessToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ServiceClient_GetUserInformation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserInformation'
type ServiceClient_GetUserInformation_Call[T interface{}] struct {
*mock.Call
}
// GetUserInformation is a helper method to define mock.On call
// - ctx context.Context
// - accessToken string
func (_e *ServiceClient_Expecter[T]) GetUserInformation(ctx interface{}, accessToken interface{}) *ServiceClient_GetUserInformation_Call[T] {
return &ServiceClient_GetUserInformation_Call[T]{Call: _e.mock.On("GetUserInformation", ctx, accessToken)}
}
func (_c *ServiceClient_GetUserInformation_Call[T]) Run(run func(ctx context.Context, accessToken string)) *ServiceClient_GetUserInformation_Call[T] {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *ServiceClient_GetUserInformation_Call[T]) Return(_a0 *T, _a1 error) *ServiceClient_GetUserInformation_Call[T] {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *ServiceClient_GetUserInformation_Call[T]) RunAndReturn(run func(context.Context, string) (*T, error)) *ServiceClient_GetUserInformation_Call[T] {
_c.Call.Return(run)
return _c
}
type mockConstructorTestingTNewServiceClient interface {
mock.TestingT
Cleanup(func())
}
// NewServiceClient creates a new instance of ServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewServiceClient[T interface{}](t mockConstructorTestingTNewServiceClient) *ServiceClient[T] {
mock := &ServiceClient[T]{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

@ -0,0 +1,85 @@
package oauth
import (
"context"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
/*
TODO:
- Реализовать рандомную генерацию state. Каждый раз при генерации ссылки (auth/link),
для этой ссылки должен генерироваться новый state. Так же при реализации этого функционала,
необходимо решить, где хранить этот временный state: MongoDB, Reddis, RAM
- Покрыть тестами генерацию рандомного state
*/
//go:generate mockery --name serviceClient
type serviceClient[T any] interface {
GetUserInformation(ctx context.Context, accessToken string) (*T, error)
}
//go:generate mockery --name oauthClient
type oauthClient interface {
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
}
type Deps[T any] struct {
Logger *logrus.Logger
ServiceClient serviceClient[T]
OAuthClient oauthClient
}
type Service[T any] struct {
logger *logrus.Logger
serviceClient serviceClient[T]
oauthClient oauthClient
state string
}
func New[T any](deps *Deps[T]) *Service[T] {
return &Service[T]{
logger: deps.Logger,
serviceClient: deps.ServiceClient,
oauthClient: deps.OAuthClient,
state: utils.GetRandomString(10),
}
}
func (receiver *Service[T]) GetUserInformationByCode(ctx context.Context, code string) (*T, error) {
token, err := receiver.oauthClient.Exchange(ctx, code)
if err != nil {
receiver.logger.Errorf("failed to exchange token on <GetSocialUserInformationByCode> of <AuthService>: %v", err)
return nil, err
}
userInformation, err := receiver.serviceClient.GetUserInformation(ctx, token.AccessToken)
if err != nil {
receiver.logger.Errorf("failed to get user information on <GetSocialUserInformationByCode> of <AuthService>: %v", err)
return nil, err
}
return userInformation, nil
}
func (receiver *Service[any]) GenerateAuthURL() string {
return receiver.oauthClient.AuthCodeURL(receiver.GetState(), oauth2.AccessTypeOffline)
}
func (receiver *Service[any]) GenerateLinkURL(accessToken string) string {
setAccessTokenParam := oauth2.SetAuthURLParam("accessToken", accessToken)
return receiver.oauthClient.AuthCodeURL(receiver.GetState(), oauth2.AccessTypeOffline, setAccessTokenParam)
}
func (receiver *Service[any]) ValidateState(state string) bool {
return state == receiver.state
}
func (receiver *Service[any]) GetState() string {
return receiver.state
}

@ -0,0 +1,167 @@
package oauth_test
import (
"context"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/oauth2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/oauth"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/service/oauth/mocks"
)
type SocialUserInformation struct {
ID string
}
var (
code = "code"
socialUserInformation = SocialUserInformation{
ID: "userid1",
}
oauthTokens = oauth2.Token{
AccessToken: "access-token-auth",
RefreshToken: "refresh-token-auth",
}
)
func TestGetUserInformationByCode(t *testing.T) {
t.Run("Успешное получение информации о пользователе oauth через код", func(t *testing.T) {
serviceClient := mocks.NewServiceClient[SocialUserInformation](t)
oauthClient := mocks.NewOauthClient(t)
oauthService := oauth.New(&oauth.Deps[SocialUserInformation]{
Logger: logrus.New(),
ServiceClient: serviceClient,
OAuthClient: oauthClient,
})
oauthCodeExchangeCall := oauthClient.EXPECT().
Exchange(mock.Anything, code).
Return(&oauthTokens, nil).
Once()
serviceClient.EXPECT().
GetUserInformation(mock.Anything, oauthTokens.AccessToken).
Return(&socialUserInformation, nil).
NotBefore(oauthCodeExchangeCall).
Once()
information, err := oauthService.GetUserInformationByCode(context.Background(), code)
assert.NoError(t, err)
assert.Equal(t, &socialUserInformation, information)
})
t.Run("Ошибка при запросе за информацией о пользователе oauth системы", func(t *testing.T) {
serviceClient := mocks.NewServiceClient[SocialUserInformation](t)
oauthClient := mocks.NewOauthClient(t)
oauthService := oauth.New(&oauth.Deps[SocialUserInformation]{
Logger: logrus.New(),
ServiceClient: serviceClient,
OAuthClient: oauthClient,
})
oauthCodeExchangeCall := oauthClient.EXPECT().
Exchange(mock.Anything, code).
Return(&oauthTokens, nil).
Once()
serviceClient.EXPECT().
GetUserInformation(mock.Anything, oauthTokens.AccessToken).
Return(nil, errors.ErrEmptyArgs).
NotBefore(oauthCodeExchangeCall).
Once()
information, err := oauthService.GetUserInformationByCode(context.Background(), code)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrEmptyArgs.Error())
assert.Nil(t, information)
})
t.Run("Ошибка при обмене ключа на токены при получении информации об oauth пользователе", func(t *testing.T) {
serviceClient := mocks.NewServiceClient[SocialUserInformation](t)
oauthClient := mocks.NewOauthClient(t)
oauthService := oauth.New(&oauth.Deps[SocialUserInformation]{
Logger: logrus.New(),
ServiceClient: serviceClient,
OAuthClient: oauthClient,
})
oauthClient.EXPECT().
Exchange(mock.Anything, code).
Return(nil, errors.ErrEmptyArgs).
Once()
serviceClient.AssertNotCalled(t, "GetUserInformation")
information, err := oauthService.GetUserInformationByCode(context.Background(), code)
assert.Error(t, err)
assert.EqualError(t, err, errors.ErrEmptyArgs.Error())
assert.Nil(t, information)
})
}
func TestGenerateURL(t *testing.T) {
generatedLink := "https://link"
accessToken := "access-token"
t.Run("Успешная генерация ссылки авторизации", func(t *testing.T) {
oauthClient := mocks.NewOauthClient(t)
oauthService := oauth.New(&oauth.Deps[any]{
OAuthClient: oauthClient,
})
oauthClient.EXPECT().AuthCodeURL(oauthService.GetState(), oauth2.AccessTypeOffline).Return(generatedLink).Once()
assert.Equal(t, generatedLink, oauthService.GenerateAuthURL())
})
t.Run("Успешная генерация ссылки привязки аккаунта", func(t *testing.T) {
oauthClient := mocks.NewOauthClient(t)
oauthService := oauth.New(&oauth.Deps[any]{
OAuthClient: oauthClient,
})
oauthClient.EXPECT().
AuthCodeURL(
oauthService.GetState(),
oauth2.AccessTypeOffline,
oauth2.SetAuthURLParam("accessToken", accessToken),
).
Return(generatedLink).
Once()
assert.Equal(t, generatedLink, oauthService.GenerateLinkURL(accessToken))
})
}
func TestGetState(t *testing.T) {
t.Run("Успешное получение state", func(t *testing.T) {
oauthService := oauth.New(&oauth.Deps[any]{})
assert.NotEmpty(t, oauthService.GetState())
assert.NotNil(t, oauthService.GetState())
assert.NotZero(t, oauthService.GetState())
})
}
func TestValidateState(t *testing.T) {
t.Run("Невалидный state", func(t *testing.T) {
oauthService := oauth.New(&oauth.Deps[any]{})
assert.Equal(t, false, oauthService.ValidateState(""))
})
t.Run("Валидный state", func(t *testing.T) {
oauthService := oauth.New(&oauth.Deps[any]{})
assert.Equal(t, true, oauthService.ValidateState(oauthService.GetState()))
})
}

@ -0,0 +1,23 @@
package utils
import (
"net/http"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
)
var clientErrors = map[int]error{
http.StatusInternalServerError: errors.ErrInvalidReturnValue,
http.StatusBadRequest: errors.ErrInvalidArgs,
http.StatusNotImplemented: errors.ErrMethodNotImplemented,
http.StatusNotFound: errors.ErrNoServerItem,
}
func DetermineClientErrorResponse(statusCode int) error {
err, ok := clientErrors[statusCode]
if !ok {
return errors.ErrInvalidReturnValue
}
return err
}

@ -0,0 +1,38 @@
package utils
import (
"net/http"
"github.com/labstack/echo/v4"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
)
var httpStatuses = map[error]int{
errors.ErrEmptyArgs: http.StatusInternalServerError,
errors.ErrInvalidArgs: http.StatusBadRequest,
errors.ErrFindRecord: http.StatusInternalServerError,
errors.ErrInsertRecord: http.StatusInternalServerError,
errors.ErrMethodNotImplemented: http.StatusNotImplemented,
errors.ErrNoRecord: http.StatusNotFound,
errors.ErrNoServerItem: http.StatusNotFound,
errors.ErrTransaction: http.StatusInternalServerError,
errors.ErrTransactionSessionStart: http.StatusInternalServerError,
errors.ErrUpdateRecord: http.StatusInternalServerError,
errors.ErrInvalidReturnValue: http.StatusInternalServerError,
}
func DetermineEchoErrorResponse(ctx echo.Context, err error, message string) error {
status, ok := httpStatuses[err]
if !ok {
return ctx.JSON(http.StatusInternalServerError, models.ResponseErrorHTTP{
StatusCode: http.StatusInternalServerError,
Message: message,
})
}
return ctx.JSON(status, models.ResponseErrorHTTP{
StatusCode: status,
Message: message,
})
}

103
internal/utils/jwt.go Normal file

@ -0,0 +1,103 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
jsonUtil "penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/json"
)
type JWT[T any] struct {
privateKey []byte
publicKey []byte
algorithm *jwt.SigningMethodRSA
expiresIn time.Duration
issuer string
audience string
}
func NewJWT[T any](configuration *models.JWTConfiguration) *JWT[T] {
return &JWT[T]{
privateKey: []byte(configuration.PrivateKey),
publicKey: []byte(configuration.PublicKey),
algorithm: &configuration.Algorithm,
expiresIn: configuration.ExpiresIn,
issuer: configuration.Issuer,
audience: configuration.Audience,
}
}
func (receiver *JWT[T]) Create(content *T) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(receiver.privateKey)
if err != nil {
return "", fmt.Errorf("failed to parse private key on <Create> of <JWT>: %w", err)
}
encoded, err := json.Marshal(content)
if err != nil {
return "", fmt.Errorf("failed to encode content to json on <Create> of <JWT>: %w", err)
}
now := time.Now().UTC()
claims := jwt.MapClaims{
"dat": string(encoded), // Our custom data.
"exp": now.Add(receiver.expiresIn).Unix(), // The expiration time after which the token must be disregarded.
"aud": receiver.audience, // Audience
"iss": receiver.issuer, // Issuer
}
token, err := jwt.NewWithClaims(receiver.algorithm, claims).SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sing on <Create> of <JWT>: %w", err)
}
return token, nil
}
func (receiver *JWT[T]) Validate(tokenString string) (*T, error) {
key, err := jwt.ParseRSAPublicKeyFromPEM(receiver.publicKey)
if err != nil {
return nil, fmt.Errorf("failed to parse rsa public key on <Validate> of <JWT>: %w", err)
}
parseCallback := func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", token.Header["alg"])
}
return key, nil
}
token, err := jwt.Parse(
tokenString,
parseCallback,
jwt.WithAudience(receiver.audience),
jwt.WithIssuer(receiver.issuer),
)
if err != nil {
return nil, fmt.Errorf("failed to parse jwt token on <Validate> of <JWT>: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return nil, errors.New("token is invalid on <Validate> of <JWT>")
}
data, ok := claims["dat"].(string)
if !ok {
return nil, errors.New("data is empty or not a string on <Validate> of <JWT>")
}
parsedData, err := jsonUtil.Parse[T](strings.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to parse data on <Validate> of <JWT>: %w", err)
}
return parsedData, nil
}

129
internal/utils/jwt_test.go Normal file

@ -0,0 +1,129 @@
package utils_test
import (
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/utils"
)
func TestJWT(t *testing.T) {
type user struct {
Name string `json:"name"`
Age int `json:"age"`
}
testUser := user{
Name: "test",
Age: 80,
}
publicKey := `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyt4XuLovUY7i12K2PIMb
QZOKn+wFFKUvxvKQDel049/+VMpHMx1FLolUKuyGp9zi6gOwjHsBPgc9oqr/eaXG
QSh7Ult7i9f+Ht563Y0er5UU9Zc5ZPSxf9O75KYD48ruGkqiFoncDqPENK4dtUa7
w0OqlN4bwVBbmIsP8B3EDC5Dof+vtiNTSHSXPx+zifKeZGyknp+nyOHVrRDhPjOh
zQzCom0MSZA/sJYmps8QZgiPA0k4Z6jTupDymPOIwYeD2C57zSxnAv0AfC3/pZYJ
bZYH/0TszRzmy052DME3zMnhMK0ikdN4nzYqU0dkkA5kb5GtKDymspHIJ9eWbUuw
gtg8Rq/LrVBj1I3UFgs0ibio40k6gqinLKslc5Y1I5mro7J3OSEP5eO/XeDLOLlO
JjEqkrx4fviI1cL3m5L6QV905xmcoNZG1+RmOg7D7cZQUf27TXqM381jkbNdktm1
JLTcMScxuo3vaRftnIVw70V8P8sIkaKY8S8HU1sQgE2LB9t04oog5u59htx2FHv4
B13NEm8tt8Tv1PexpB4UVh7PIualF6SxdFBrKbraYej72wgjXVPQ0eGXtGGD57j8
DUEzk7DK2OvIWhehlVqtiRnFdAvdBj2ynHT2/5FJ/Zpd4n5dKGJcQvy1U1qWMs+8
M7AHfWyt2+nZ04s48+bK3yMCAwEAAQ==
-----END PUBLIC KEY-----`
privateKey := `-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAyt4XuLovUY7i12K2PIMbQZOKn+wFFKUvxvKQDel049/+VMpH
Mx1FLolUKuyGp9zi6gOwjHsBPgc9oqr/eaXGQSh7Ult7i9f+Ht563Y0er5UU9Zc5
ZPSxf9O75KYD48ruGkqiFoncDqPENK4dtUa7w0OqlN4bwVBbmIsP8B3EDC5Dof+v
tiNTSHSXPx+zifKeZGyknp+nyOHVrRDhPjOhzQzCom0MSZA/sJYmps8QZgiPA0k4
Z6jTupDymPOIwYeD2C57zSxnAv0AfC3/pZYJbZYH/0TszRzmy052DME3zMnhMK0i
kdN4nzYqU0dkkA5kb5GtKDymspHIJ9eWbUuwgtg8Rq/LrVBj1I3UFgs0ibio40k6
gqinLKslc5Y1I5mro7J3OSEP5eO/XeDLOLlOJjEqkrx4fviI1cL3m5L6QV905xmc
oNZG1+RmOg7D7cZQUf27TXqM381jkbNdktm1JLTcMScxuo3vaRftnIVw70V8P8sI
kaKY8S8HU1sQgE2LB9t04oog5u59htx2FHv4B13NEm8tt8Tv1PexpB4UVh7PIual
F6SxdFBrKbraYej72wgjXVPQ0eGXtGGD57j8DUEzk7DK2OvIWhehlVqtiRnFdAvd
Bj2ynHT2/5FJ/Zpd4n5dKGJcQvy1U1qWMs+8M7AHfWyt2+nZ04s48+bK3yMCAwEA
AQKCAgEAhodpK7MsFeWvQC3Rs6ctt/rjftHBPMOeP0wzg0ZBoau0uP264YaTjhy7
mAtp8H9matEvjrkzRbL/iJPk/wKTyjnSLfdEoqQFfOsEh09B/iXa1FIIWY569s2u
WB5PjgvQgdbkThX1vC+VuWmNgdz6Pq7su/Pea/+h/jKZyx2yGHHFn/QyzZH3dKD8
e3vGT8B4kRgKwrYVSf2Y+T+sXtdWgOfpWlT+RPpHgg7QauX9dexPClrP8M3gOmRM
vGkjU1NOd1m7939ugGjOnYrTcTdh4S4Q95L5hbuYwVGyrxqiqkdl8iWeOx4Fa287
+iXp5i3lJKdyMLCnytsp5GHu+2OqFKyYQli23eMEEiTq/7PrzJas0BD3LfuT55Ht
UCwI/pRdgHvc/xEHqr7eF0C3f+PPG9/C85StDbm9WqhCVQ9nGt2ezkLeUSM/DBAh
DgI/LDFqRwLlIDrhkTT7BJGz6+2cmHwV80+eGPG2WzjpI619qhqgqB0fGBjLlVcZ
qoHy0K6NXuBqaoPOQq0TGkhl3SjurSe9EXeZHrrCT3LcSAIT7ZYoZDYuIvKBj7Sh
7r/wdYS9nzsBhU0xeGzfAs+5yxDCp1/GzLK0H8LlJcjJOxqArtEzf55v7ZBB8erR
sqmbpGoQAwzwyw1zosmhzQwZRlAMPNi0yfnjfi8yQu4kZchyJyECggEBAOStATj0
JNYWrPoHSgdW+NkzMRNIjjkHkUM/zs9F1bIlYwYDzmduXUoLChCHcjbOyE2tsPi8
eFbyJ0vpMa0ZgoQmAnqUhYOwceu/tmI2CE7jLB2luq9oFhQIblKR6Fi8TyvPzn4N
Q4iD1I2VjffSSQher+hNVdLmpRkP8s2UiY7OQOZMBWKNqfORddQWcXp3Wrg2Lkbd
7KcAtaMLYWg2W3mRdz6dnsqjMomRMi5arhroG3CtIpb62uiEdq2ZwyGF/Awon/kr
/XnfRLQeH0xVFPuVS/EbP6Ipq0TiieElTh4erhUIbmLZg7B5Fe9z1c528GUzTxhP
geQwN3bS5q71/f8CggEBAOMbosN7S+merANPzCOnRruLDPXukW+u20t/8CrOibJM
MO0embITOJfEdG4jBVRwnm5qacojuzFwfD7C18fJ1Hty010yQkjnB/zch3i8Fjx1
vtsWnYOfbViuIzuEi+9bPWRlMZh504zDjgqo8P24JU5qziw/ySLfMZAX7iNsohRB
R+bBdP933kPoCo5ehSj4QyVgRIWN751x5sZ0eyCUTZIw9OswuOmsmnlw4nMsqWIx
OXlARVkbA97+1pp21pAromekE/bzN8Qo4pn4inZTTy9yAeAvSp+vScCiaVJ4n+ag
WAgLeQBLxqRCU6BMvKiRjQ8dBMAn1DjKCrlV+5zFZt0CggEAd8TZEBBnPq4vuOCa
eE+oFHKIcJYez2XUQkmoMs1byGtmet8BexDF0aMIiXG3c1dId87SEuT7jmZUCKFB
gG0M+9PAlp01dKy0bgpCJxwvq8m18G094uL8NU/ZIGwFKnyuZr73YvPlfBm3+NPs
wHCmCbk2HtBqdASTUhYVUHFMvrvuJ/CHHYAfFFAKS6PZmY/rtvHBuSJA8ZMgjx3F
zcQykvCKaQQ7B90D+iNPChI6gCMzRAeaR0Np5kCCvBf9qJA5W9DnQKU2pF8457Gj
KOKjE8W1ObnQ0UlLx89y8bYNPR9Kg/+feSx9ma9BuuGLiRCohgiik5QI7xAF7Lk3
U0nJ1wKCAQAmkbjwre3UfSgFX/XxUCVJEHJhCeUVLIL9rXqiKnVkHGBqxLmhbnY8
ABct5TCwiHe/lL7mn27ZFJtlJT30Jii51mRi/XgYXXQT03gGXxr/pZeGKa8SfW7a
kqhVIUuKmNoyRKVJmdb9nvBuiwZycGWVjbn59dM44uLN7+J3jalw+y002UH/aOIM
cknop9DBhngQzuqUK+i3unJQ3dNTUxxhaYMOtjWRKckKOsuad8lEbcuu9eVRHq9n
navgi7IgxehM5aamV+PuomrpbzZEph1al2gOJLntqJ1D49EzOl0dk7mflCM2k6fm
mYUOQjn//sgP+wOlhp4aDuYHV7zlgPjZAoIBAQDXPUl6NeA2ZMWbSO+WRc8zzjQ9
qyxRA7g3ZSu+E5OqkxfwayXr/kAVKQNHJvn5wr9rLFhEF6CkBJ7XgOrHN0RjgXq2
z0DpwG5JEFMeqkQWI+rVJ+ZJ4g0SAa9k39+WDxQhpZM8/IlkuIYqRI0mlcHwxhkG
7JhkLtELhlxaGobAIinWiskKqX85tzZtCLe1wkErWOCueWviiuoCY2HWfELoA5+4
wAvKspBO6oa+R2JtjA0nE72jKWuIz4m0QaCE7yInyCG9ikrBHSh/85eMu37nqegU
ziOydfDNcQp17fBjy8NVeQBjdjxVYejl8pKAVcQP9iM4vIyRIx0Ersv1fySA
-----END RSA PRIVATE KEY-----`
publicKey = strings.Replace(publicKey, "\t", "", -1)
privateKey = strings.Replace(privateKey, "\t", "", -1)
jwt := utils.NewJWT[user](&models.JWTConfiguration{
PrivateKey: privateKey,
PublicKey: publicKey,
Algorithm: *jwt.SigningMethodRS256,
ExpiresIn: 15 * time.Minute,
Issuer: "issuer1",
Audience: "audience1",
})
t.Run("Успешная генерация токена", func(t *testing.T) {
assert.NotPanics(t, func() {
token, err := jwt.Create(&testUser)
assert.NoError(t, err)
assert.NotZero(t, token)
assert.NotEmpty(t, token)
})
})
t.Run("Успешная валидация токена", func(t *testing.T) {
assert.NotPanics(t, func() {
token, err := jwt.Create(&testUser)
isNoError := assert.NoError(t, err)
if isNoError {
parsedUser, err := jwt.Validate(token)
assert.NoError(t, err)
assert.NotNil(t, parsedUser)
assert.Equal(t, &testUser, parsedUser)
}
})
})
}

@ -0,0 +1,60 @@
package utils
import (
"fmt"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/validate"
)
func ValidateConfigurationURLs(config *models.ServiceConfiguration) error {
if err := validateGoogleURLs(&config.Google.URL); err != nil {
return err
}
if err := validateAmocrmURLs(&config.Amocrm.URL); err != nil {
return err
}
return validateAuthMicroserviceURLs(&config.AuthMicroservice.URL)
}
func validateGoogleURLs(urls *models.GoogleURL) error {
if !validate.URL(urls.OAuthHost) {
return fmt.Errorf("invalid google oauth host: %s", urls.OAuthHost)
}
return nil
}
func validateAmocrmURLs(urls *models.AmocrmURL) error {
if !validate.URL(urls.OAuthHost) {
return fmt.Errorf("invalid amocrm oauth host: %s", urls.OAuthHost)
}
if !validate.URL(urls.UserInfo) {
return fmt.Errorf("invalid amocrm user info url: %s", urls.UserInfo)
}
if !validate.URL(urls.AccessToken) {
return fmt.Errorf("invalid amocrm access token url: %s", urls.AccessToken)
}
return nil
}
func validateAuthMicroserviceURLs(urls *models.AuthMicroServiceURL) error {
if !validate.URL(urls.Exchange) {
return fmt.Errorf("invalid auth microservice exchange url: %s", urls.Exchange)
}
if !validate.URL(urls.Register) {
return fmt.Errorf("invalid auth register url: %s", urls.Register)
}
if !validate.URL(urls.User) {
return fmt.Errorf("invalid auth user url: %s", urls.User)
}
return nil
}

@ -0,0 +1 @@
[{ "delete": "amocrm", "deletes": [{ "q": {} }] }]

@ -0,0 +1,85 @@
[
{
"insert": "amocrm",
"ordered": true,
"documents": [
{
"amocrmId": "1",
"userId": "1",
"information": {
"id": 30228997,
"name": "ООО ПЕНА",
"subdomain": "penadigital",
"created_at": 1683680509,
"created_by": 0,
"updated_at": 1683680509,
"updated_by": 0,
"current_user_id": 8110726,
"country": "RU",
"customers_mode": "disabled",
"is_unsorted_on": true,
"is_loss_reason_enabled": true,
"is_helpbot_enabled": false,
"is_technical_account": true,
"contact_name_display_order": 1,
"amojo_id": "",
"uuid": "",
"version": 0,
"_links": { "self": { "href": "https://penadigital.amocrm.ru/api/v4/account" } },
"_embedded": {
"amojo_rights": { "can_direct": false, "can_create_groups": false },
"users_groups": null,
"task_types": null,
"entity_names": {
"leads": {
"ru": {
"gender": "",
"plural_form": {
"dative": "",
"default": "",
"genitive": "",
"accusative": "",
"instrumental": "",
"prepositional": ""
},
"singular_form": {
"dative": "",
"default": "",
"genitive": "",
"accusative": "",
"instrumental": "",
"prepositional": ""
}
},
"en": {
"singular_form": { "default": "" },
"plural_form": { "default": "" },
"gender": ""
},
"es": {
"singular_form": { "default": "" },
"plural_form": { "default": "" },
"gender": ""
}
}
},
"datetime_settings": {
"date_pattern": "",
"short_date_pattern": "",
"short_time_pattern": "",
"date_formant": "",
"time_format": "",
"timezone": "",
"timezone_offset": ""
}
}
},
"audit": {
"createdAt": "2022-12-31T00:00:00.000Z",
"updatedAt": "2022-12-31T00:00:00.000Z",
"deleted": false
}
}
]
}
]

15
pkg/array/contains.go Normal file

@ -0,0 +1,15 @@
package array
func Contains[T any](array []T, callback func(T, int, []T) bool) bool {
isContains := false
for index, element := range array {
if callback(element, index, array) {
isContains = true
break
}
}
return isContains
}

153
pkg/array/contains_test.go Normal file

@ -0,0 +1,153 @@
package array_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/array"
)
func TestContains(t *testing.T) {
testCasesWithPrimitives := []struct {
name string
inputArray []any
inputCallback func(any, int, []any) bool
expect bool
}{
{
name: "Проверка наличие строк по значению",
inputArray: []any{"test12", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test12", "test2"})
return element == "test12"
},
expect: true,
},
{
name: "Проверка наличие строк по значению (неудачная)",
inputArray: []any{"test1", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test1", "test2"})
return element == "tttt3"
},
expect: false,
},
{
name: "Проверка наличие строк по индексу",
inputArray: []any{"test1", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test1", "test2"})
return index == 1
},
expect: true,
},
{
name: "Проверка наличие чисел по значению",
inputArray: []any{1, 4},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{1, 4})
return element == 1
},
expect: true,
},
{
name: "Проверка наличие строк по значению с несколькими схожими значениями",
inputArray: []any{"test1", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test1", "test2"})
return strings.Contains(element.(string), "test")
},
expect: true,
},
}
testCasesWithObjects := []struct {
name string
inputArray []struct{ Name string }
inputCallback func(struct{ Name string }, int, []struct{ Name string }) bool
expect bool
}{
{
name: "Проверка наличие объектов по индексу",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return index == 1
},
expect: true,
},
{
name: "Проверка наличие объектов по имени (неудачная)",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return element.Name == "tttt"
},
expect: false,
},
{
name: "Проверка наличие объектов по значению поля",
inputArray: []struct{ Name string }{
{Name: "test1a"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1a"},
{Name: "test2"},
})
return element.Name == "test1a"
},
expect: true,
},
{
name: "Проверка наличие объектов по совпадению значения поля",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return strings.Contains(element.Name, "test")
},
expect: true,
},
}
for _, test := range testCasesWithPrimitives {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expect, array.Contains(test.inputArray, test.inputCallback))
})
}
for _, test := range testCasesWithObjects {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expect, array.Contains(test.inputArray, test.inputCallback))
})
}
}

13
pkg/array/filter.go Normal file

@ -0,0 +1,13 @@
package array
func Filter[T any](array []T, callback func(T, int, []T) bool) []T {
filteredArray := make([]T, 0, len(array))
for index, element := range array {
if callback(element, index, array) {
filteredArray = append(filteredArray, element)
}
}
return filteredArray
}

134
pkg/array/filter_test.go Normal file

@ -0,0 +1,134 @@
package array_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/array"
)
func TestFilter(t *testing.T) {
testCasesWithPrimitives := []struct {
name string
inputArray []any
inputCallback func(any, int, []any) bool
expect []any
}{
{
name: "Фильтрация массива строк по значению",
inputArray: []any{"test125", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test125", "test2"})
return element == "test125"
},
expect: []any{"test125"},
},
{
name: "Фильтрация массива строк по индексу",
inputArray: []any{"test1", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test1", "test2"})
return index == 1
},
expect: []any{"test2"},
},
{
name: "Фильтрация массива чисел по значению",
inputArray: []any{1, 4},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{1, 4})
return element == 1
},
expect: []any{1},
},
{
name: "Фильтрация массива строк по значению с несколькими схожими значениями",
inputArray: []any{"test1", "test2"},
inputCallback: func(element any, index int, array []any) bool {
assert.Equal(t, array, []any{"test1", "test2"})
return strings.Contains(element.(string), "test")
},
expect: []any{"test1", "test2"},
},
}
testCasesWithObjects := []struct {
name string
inputArray []struct{ Name string }
inputCallback func(struct{ Name string }, int, []struct{ Name string }) bool
expect []struct{ Name string }
}{
{
name: "Фильтрация массива объектов по индексу",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return index == 1
},
expect: []struct{ Name string }{
{Name: "test2"},
},
},
{
name: "Фильтрация массива объектов по значению поля",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return element.Name == "test1"
},
expect: []struct{ Name string }{
{Name: "test1"},
},
},
{
name: "Фильтрация массива объектов по совпадению значения поля",
inputArray: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
inputCallback: func(element struct{ Name string }, index int, array []struct{ Name string }) bool {
assert.Equal(t, array, []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
})
return strings.Contains(element.Name, "test")
},
expect: []struct{ Name string }{
{Name: "test1"},
{Name: "test2"},
},
},
}
for _, test := range testCasesWithPrimitives {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expect, array.Filter(test.inputArray, test.inputCallback))
})
}
for _, test := range testCasesWithObjects {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expect, array.Filter(test.inputArray, test.inputCallback))
})
}
}

31
pkg/client/client.go Normal file

@ -0,0 +1,31 @@
package client
import (
"context"
)
type RequestSettings struct {
URL string
Headers map[string]string
QueryParams map[string]string
Body any
Formdata any
}
type Response[T any, R any] struct {
StatusCode int
Error *R
Body *T
}
func Post[T any, R any](ctx context.Context, settings *RequestSettings) (*Response[T, R], error) {
request := buildRequest(ctx, settings)
return makeRequest[T, R](settings.URL, request.Post)
}
func Get[T any, R any](ctx context.Context, settings *RequestSettings) (*Response[T, R], error) {
request := buildRequest(ctx, settings)
return makeRequest[T, R](settings.URL, request.Get)
}

62
pkg/client/request.go Normal file

@ -0,0 +1,62 @@
package client
import (
"context"
"github.com/go-resty/resty/v2"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/convert"
)
func buildRequest(ctx context.Context, settings *RequestSettings) *resty.Request {
request := resty.New().R().SetContext(ctx)
if settings == nil {
return request
}
if settings.Body != nil {
request.SetBody(settings.Body)
}
if settings.Formdata != nil {
formdata, _ := convert.ObjectToStringMap(settings.Formdata, "formdata")
request.SetFormData(formdata)
}
if settings.QueryParams != nil {
request.SetQueryParams(settings.QueryParams)
}
request.SetHeaders(settings.Headers)
return request
}
func makeRequest[T any, R any](url string, requestMethod func(url string) (*resty.Response, error)) (*Response[T, R], error) {
responseInstance, err := requestMethod(url)
if err != nil {
return nil, err
}
if responseInstance.IsError() {
responseBody, parseErr := parseResponse[R](responseInstance.Body(), responseInstance.RawResponse)
if parseErr != nil {
return nil, parseErr
}
return &Response[T, R]{
StatusCode: responseInstance.StatusCode(),
Error: responseBody,
}, nil
}
responseBody, parseErr := parseResponse[T](responseInstance.Body(), responseInstance.RawResponse)
if parseErr != nil {
return nil, parseErr
}
return &Response[T, R]{
StatusCode: responseInstance.StatusCode(),
Body: responseBody,
}, nil
}

28
pkg/client/response.go Normal file

@ -0,0 +1,28 @@
package client
import (
"bytes"
"net/http"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/json"
)
func parseResponse[T any](body []byte, response *http.Response) (*T, error) {
isJSONResponse := response.Header.Get("Content-Type") == "application/json"
if !isJSONResponse {
responseBody, unmarshalErr := json.Unmarshal[T](body)
if unmarshalErr != nil {
return nil, unmarshalErr
}
return responseBody, nil
}
responseBody, parseErr := json.Parse[T](bytes.NewReader(body))
if parseErr != nil {
return nil, parseErr
}
return responseBody, nil
}

52
pkg/closer/closer.go Normal file

@ -0,0 +1,52 @@
package closer
import (
"context"
"fmt"
"sync"
"github.com/sirupsen/logrus"
)
type Callback func(ctx context.Context) error
type Closer struct {
mutex sync.Mutex
callbacks []Callback
logger *logrus.Logger
}
func New(logger *logrus.Logger) *Closer {
return &Closer{logger: logger}
}
func (receiver *Closer) Add(callback Callback) {
receiver.mutex.Lock()
defer receiver.mutex.Unlock()
receiver.callbacks = append(receiver.callbacks, callback)
}
func (receiver *Closer) Close(ctx context.Context) error {
receiver.mutex.Lock()
defer receiver.mutex.Unlock()
complete := make(chan struct{}, 1)
go func() {
for index, callback := range receiver.callbacks {
if err := callback(ctx); err != nil {
receiver.logger.Errorf("[! (%d)] %v", index, err)
}
}
complete <- struct{}{}
}()
select {
case <-complete:
return nil
case <-ctx.Done():
return fmt.Errorf("shutdown cancelled: %v", ctx.Err())
}
}

23
pkg/env/parse.go vendored Normal file

@ -0,0 +1,23 @@
package env
import (
"context"
"fmt"
"log"
envLoader "github.com/joho/godotenv"
envParser "github.com/sethvargo/go-envconfig"
)
func Parse[T interface{}](envFilePath string) (*T, error) {
var configuration T
if err := envLoader.Load(envFilePath); err != nil {
log.Printf("load local env file error: %s", err.Error())
}
if err := envParser.Process(context.Background(), &configuration); err != nil {
return nil, fmt.Errorf("parsing env error: %s", err.Error())
}
return &configuration, nil
}

16
pkg/json/encode.go Normal file

@ -0,0 +1,16 @@
package json
import (
"bytes"
"encoding/json"
)
func EncodeBuffer(body interface{}) (*bytes.Buffer, error) {
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(body); err != nil {
return nil, err
}
return buffer, nil
}

26
pkg/json/parse.go Normal file

@ -0,0 +1,26 @@
package json
import (
"encoding/json"
"io"
)
func Parse[T any](reader io.Reader) (*T, error) {
jsonData := new(T)
if err := json.NewDecoder(reader).Decode(jsonData); err != nil {
return nil, err
}
return jsonData, nil
}
func Unmarshal[T any](data []byte) (*T, error) {
unmarshaled := new(T)
if err := json.Unmarshal(data, unmarshaled); err != nil {
return nil, err
}
return unmarshaled, nil
}

20
pkg/mongo/config.go Normal file

@ -0,0 +1,20 @@
package mongo
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type Configuration struct {
Host string `env:"MONGO_HOST,default=localhost"`
Port string `env:"MONGO_PORT,default=27017"`
User string `env:"MONGO_USER,required"`
Password string `env:"MONGO_PASSWORD,required"`
Auth string `env:"MONGO_AUTH,required"`
DatabaseName string `env:"MONGO_DB_NAME,required"`
}
type RequestSettings struct {
Driver *mongo.Collection
Filter primitive.M
}

64
pkg/mongo/connection.go Normal file

@ -0,0 +1,64 @@
package mongo
import (
"context"
"fmt"
"log"
"net"
"net/url"
"time"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type ConnectDeps struct {
Configuration *Configuration
Timeout time.Duration
}
func Connect(ctx context.Context, deps *ConnectDeps) (*mongo.Database, error) {
if deps == nil {
return nil, ErrEmptyArgs
}
mongoURI := &url.URL{
Scheme: "mongodb",
User: url.UserPassword(deps.Configuration.User, deps.Configuration.Password),
Host: net.JoinHostPort(deps.Configuration.Host, deps.Configuration.Port),
}
mongoURIWithParams := fmt.Sprintf("%s/?authSource=admin", mongoURI.String())
cmdMonitor := &event.CommandMonitor{
Started: func(_ context.Context, evt *event.CommandStartedEvent) {
log.Println(evt.Command)
},
Succeeded: func(_ context.Context, evt *event.CommandSucceededEvent) {
log.Println(evt.Reply)
},
Failed: func(_ context.Context, evt *event.CommandFailedEvent) {
log.Println(evt.Failure)
},
}
ticker := time.NewTicker(1 * time.Second)
timeoutExceeded := time.After(deps.Timeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
connection, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURIWithParams).SetMonitor(cmdMonitor))
if err == nil {
return connection.Database(deps.Configuration.DatabaseName), nil
}
log.Printf("failed to connect to db <%s>: %s", mongoURI.String(), err.Error())
case <-timeoutExceeded:
return nil, fmt.Errorf("db connection <%s> failed after %d timeout", mongoURI, deps.Timeout)
}
}
}

7
pkg/mongo/errors.go Normal file

@ -0,0 +1,7 @@
package mongo
import "errors"
var (
ErrEmptyArgs = errors.New("arguments are empty")
)

55
pkg/mongo/find.go Normal file

@ -0,0 +1,55 @@
package mongo
import (
"context"
"log"
)
func Find[T any](ctx context.Context, settings *RequestSettings) ([]T, error) {
if settings == nil {
return []T{}, ErrEmptyArgs
}
results := make([]T, 0)
cursor, err := settings.Driver.Find(ctx, settings.Filter)
if err != nil {
return []T{}, err
}
defer func() {
if err := cursor.Close(ctx); err != nil {
log.Printf("failed to close cursor: %v", err)
}
}()
for cursor.Next(ctx) {
result := new(T)
if err := cursor.Decode(result); err != nil {
return []T{}, err
}
results = append(results, *result)
}
if err := cursor.Err(); err != nil {
return []T{}, err
}
return results, nil
}
func FindOne[T any](ctx context.Context, settings *RequestSettings) (*T, error) {
if settings == nil {
return nil, ErrEmptyArgs
}
result := new(T)
if err := settings.Driver.FindOne(ctx, settings.Filter).Decode(result); err != nil {
return nil, err
}
return result, nil
}

35
pkg/utils/fields_count.go Normal file

@ -0,0 +1,35 @@
package utils
import (
"reflect"
)
func GetFilledFieldsCount(object interface{}) int {
filledFieldsCount := int(0)
if object == nil {
return 0
}
value := reflect.ValueOf(object)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
fieldsCount := value.NumField()
if fieldsCount < 1 {
return 0
}
for index := 0; index < fieldsCount; index++ {
field := value.Field(index)
if field.IsZero() {
continue
}
filledFieldsCount++
}
return filledFieldsCount
}

@ -0,0 +1,64 @@
package utils_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
func TestGetFilledFieldsCount(t *testing.T) {
name := "name"
age := uint(10)
t.Run("Получение количества заполненных полей структуры с пустой структурой", func(t *testing.T) {
assert.Equal(t, 0, utils.GetFilledFieldsCount(struct{}{}))
})
t.Run("Получение количества заполненных полей структуры с nil", func(t *testing.T) {
assert.Equal(t, 0, utils.GetFilledFieldsCount(nil))
})
t.Run("Получение количества заполненных полей структуры с указателем на пустую структуру", func(t *testing.T) {
assert.Equal(t, 0, utils.GetFilledFieldsCount(&struct{}{}))
})
t.Run("Получение количества заполненных полей структуры с заполненной структурой", func(t *testing.T) {
assert.Equal(t, 1, utils.GetFilledFieldsCount(struct {
name *string
age *uint
}{
name: &name,
}))
})
t.Run("Получение количества заполненных полей структуры с указателем на заполненную структурой", func(t *testing.T) {
assert.Equal(t, 2, utils.GetFilledFieldsCount(&struct {
name *string
age *uint
}{
name: &name,
age: &age,
}))
})
t.Run("Получение количества заполненных полей структуры с заполненную структурой не опциональных значений", func(t *testing.T) {
assert.Equal(t, 2, utils.GetFilledFieldsCount(&struct {
name *string
age uint
}{
name: &name,
age: age,
}))
})
t.Run("Получение количества заполненных полей структуры с не заполненную структурой не опциональных значений", func(t *testing.T) {
assert.Equal(t, 0, utils.GetFilledFieldsCount(&struct {
name string
age uint
}{
name: "",
age: 0,
}))
})
}

13
pkg/utils/merge_maps.go Normal file

@ -0,0 +1,13 @@
package utils
func MergeMaps[Map ~map[K]V, K comparable, V any](maps ...Map) Map {
merged := make(Map)
for _, currentMap := range maps {
for key, value := range currentMap {
merged[key] = value
}
}
return merged
}

@ -0,0 +1,50 @@
package utils_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
func TestMergeMaps(t *testing.T) {
t.Run("Соединение двух пустых map", func(t *testing.T) {
assert.Equal(t, map[string]interface{}{}, utils.MergeMaps(map[string]interface{}{}, map[string]interface{}{}))
})
t.Run("Соединение несколько пустых map", func(t *testing.T) {
assert.Equal(t, map[string]interface{}{}, utils.MergeMaps(map[string]interface{}{}, map[string]interface{}{}, map[string]interface{}{}))
})
t.Run("Соединение заполненных map", func(t *testing.T) {
assert.Equal(t,
map[string]interface{}{
"test1": "1",
"test2": "1",
"test3": "1",
"test4": "1",
},
utils.MergeMaps(
map[string]interface{}{"test1": "1"},
map[string]interface{}{"test2": "1"},
map[string]interface{}{"test3": "1", "test4": "1"},
),
)
})
t.Run("Соединение заполненных map с одинаковыми ключами", func(t *testing.T) {
assert.Equal(t,
map[string]interface{}{
"test1": "1",
"test2": "1",
"test3": "1",
"test4": "1",
},
utils.MergeMaps(
map[string]interface{}{"test1": "1"},
map[string]interface{}{"test2": "1", "test1": "1"},
map[string]interface{}{"test3": "1", "test4": "1"},
),
)
})
}

33
pkg/utils/random.go Normal file

@ -0,0 +1,33 @@
package utils
import (
"math/rand"
"time"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIndexBits = 6 // 6 bits to represent a letter index
letterIndexMask = 1<<letterIndexBits - 1 // All 1-bits, as many as letterIdxBits
letterIndexMax = 63 / letterIndexBits // # of letter indices fitting in 63 bits
)
func GetRandomString(size int) string {
src := rand.NewSource(time.Now().UnixNano())
bytes := make([]byte, size)
for bytesIndex, cache, remain := size-1, src.Int63(), letterIndexMax; bytesIndex >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIndexMax
}
if letterIndex := int(cache & letterIndexMask); letterIndex < len(letterBytes) {
bytes[bytesIndex] = letterBytes[letterIndex]
bytesIndex--
}
cache >>= letterIndexBits
remain--
}
return string(bytes)
}

22
pkg/utils/random_test.go Normal file

@ -0,0 +1,22 @@
package utils_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/utils"
)
func TestGetRandomString(t *testing.T) {
t.Run("Создание рандомной строки с размерностью 0", func(t *testing.T) {
randomString := utils.GetRandomString(0)
assert.Equal(t, 0, len(randomString))
})
t.Run("Создание рандомной строки", func(t *testing.T) {
randomString := utils.GetRandomString(8)
assert.Equal(t, 8, len(randomString))
})
}

7
pkg/validate/string.go Normal file

@ -0,0 +1,7 @@
package validate
import "strings"
func IsStringEmpty(text string) bool {
return strings.TrimSpace(text) == ""
}

@ -0,0 +1,16 @@
package validate_test
import (
"testing"
"github.com/stretchr/testify/assert"
"penahub.gitlab.yandexcloud.net/pena-services/pena-social-auth/pkg/validate"
)
func TestIsStringEmpty(t *testing.T) {
assert.True(t, validate.IsStringEmpty(""))
assert.True(t, validate.IsStringEmpty(" "))
assert.True(t, validate.IsStringEmpty(" "))
assert.False(t, validate.IsStringEmpty("tett"))
assert.False(t, validate.IsStringEmpty(" t "))
}

Some files were not shown because too many files have changed in this diff Show More