Compare commits

..

19 Commits

Author SHA1 Message Date
bakito
a73f696ef6 update readme #9 2021-04-18 22:30:25 +02:00
bakito
2e93920931 run on startup #10 2021-04-18 22:20:08 +02:00
Marc Brugger
3edb5222d6 Initial setup (#11)
automatically setup new AdGuardHome instances #9
2021-04-18 22:03:57 +02:00
bakito
d58c8f115e log status 2021-04-18 19:32:03 +02:00
Marc Brugger
f8dd7e6136 Update issue templates 2021-04-18 18:40:43 +02:00
bakito
9fd3694237 add support debug messages 2021-04-18 18:28:35 +02:00
bakito
258ecae016 fix filter synch error 2021-04-18 18:04:53 +02:00
Marc Brugger
04a912fb56 Update issue templates 2021-04-18 11:03:44 +02:00
Marc Brugger
cd75a5f46e Merge pull request #8 from bakito/dependabot/go_modules/github.com/golang/mock-1.5.0
Bump github.com/golang/mock from 1.3.1 to 1.5.0
2021-04-12 12:28:55 +02:00
dependabot[bot]
87c578a07b Bump github.com/golang/mock from 1.3.1 to 1.5.0
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.3.1 to 1.5.0.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/1.3.1...v1.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 10:16:48 +00:00
Marc Brugger
28782fc364 Merge pull request #7 from bakito/dependabot/go_modules/github.com/onsi/ginkgo-1.16.1
Bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1
2021-04-12 10:15:17 +02:00
Marc Brugger
7470a11547 Merge pull request #6 from bakito/dependabot/go_modules/github.com/go-resty/resty/v2-2.6.0
Bump github.com/go-resty/resty/v2 from 2.4.0 to 2.6.0
2021-04-12 10:12:59 +02:00
dependabot[bot]
0d35a77e1b Bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v1.16.0...v1.16.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 08:11:52 +00:00
dependabot[bot]
bc8fc796ec Bump github.com/go-resty/resty/v2 from 2.4.0 to 2.6.0
Bumps [github.com/go-resty/resty/v2](https://github.com/go-resty/resty) from 2.4.0 to 2.6.0.
- [Release notes](https://github.com/go-resty/resty/releases)
- [Commits](https://github.com/go-resty/resty/compare/v2.4.0...v2.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 08:11:21 +00:00
bakito
da3b037009 add sync tests 2021-04-11 18:35:03 +02:00
bakito
4921af09a5 add client tests 2021-04-11 16:13:37 +02:00
bakito
e7a2604268 prepare sync and client tests 2021-04-11 11:56:55 +02:00
bakito
cb624ea52b extend tests 2021-04-11 10:51:24 +02:00
bakito
57612bae1f log enable for filters 2021-04-10 14:48:02 +02:00
29 changed files with 1806 additions and 124 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. What version of AdGuardHome sync used?
2. What version of AdGuardHome us used?
3. How does the configuration look?
4. What is the error message?
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add log files or json responses from AdGuardHome to help explain your problem.
**Additional context**
Add any other context about the problem here.

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Test - name: Test
run: go test ./... -coverprofile=coverage.out run: make test
- name: Send coverage - name: Send coverage
uses: shogo82148/actions-goveralls@v1 uses: shogo82148/actions-goveralls@v1

View File

@@ -16,10 +16,13 @@ tidy:
go mod tidy go mod tidy
# Run tests # Run tests
test: tidy fmt vet test: mocks tidy fmt vet
go test ./... -coverprofile=coverage.out go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out go tool cover -func=coverage.out
mocks: mockgen
mockgen -destination pkg/mocks/client/mock.go github.com/bakito/adguardhome-sync/pkg/client Client
release: semver release: semver
@version=$$(semver); \ @version=$$(semver); \
git tag -s $$version -m"Release $$version" git tag -s $$version -m"Release $$version"
@@ -31,4 +34,9 @@ test-release:
semver: semver:
ifeq (, $(shell which semver)) ifeq (, $(shell which semver))
$(shell go get -u github.com/bakito/semver) $(shell go get -u github.com/bakito/semver)
endif
mockgen:
ifeq (, $(shell which mockgen))
$(shell go get github.com/golang/mock/mockgen@v1.5)
endif endif

View File

@@ -1,6 +1,7 @@
[![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml) [![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync) [![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync)
[![Coverage Status](https://coveralls.io/repos/github/bakito/adguardhome-sync/badge.svg?branch=main)](https://coveralls.io/github/bakito/adguardhome-sync?branch=main) [![Coverage Status](https://coveralls.io/repos/github/bakito/adguardhome-sync/badge.svg?branch=main)](https://coveralls.io/github/bakito/adguardhome-sync?branch=main)
# AdGuardHome sync # AdGuardHome sync
Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to a replica instance. Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to a replica instance.
@@ -13,6 +14,13 @@ Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to
- Services - Services
- Clients - Clients
### Setup of initial instances
New AdGuardHome replica instances can be automatically installed if enabled via the config autoSetup.
During automatic installation, the admin interface will be listening on port 3000 in runtime.
To skip automatic setup
## Install ## Install
```bash ```bash
@@ -21,7 +29,7 @@ go get -u github.com/bakito/adguardhome-sync
## Prerequisites ## Prerequisites
Both the origin and replica mist be initially setup via the Adguard Home installation wizard. Both the origin instance must be initially setup via the AdguardHome installation wizard.
## Run ## Run
@@ -55,6 +63,7 @@ docker run -d \
## docker compose ## docker compose
### config file ### config file
```yaml ```yaml
--- ---
version: "2.1" version: "2.1"
@@ -90,7 +99,9 @@ services:
- REPLICA1_USERNAME=username - REPLICA1_USERNAME=username
- REPLICA1_PASSWORD=password - REPLICA1_PASSWORD=password
- REPLICA1_APIPATH=/some/path/control - REPLICA1_APIPATH=/some/path/control
# - REPLICA1_AUTOSETUP=true # if true, AdGuardHome is automatically initialized.
- CRON=*/10 * * * * # run every 10 minutes - CRON=*/10 * * * * # run every 10 minutes
- RUNONSTART=true
ports: ports:
- 8080:8080 - 8080:8080
restart: unless-stopped restart: unless-stopped
@@ -104,6 +115,9 @@ location: $HOME/.adguardhome-sync.yaml
# cron expression to run in daemon mode. (default; "" = runs only once) # cron expression to run in daemon mode. (default; "" = runs only once)
cron: "*/10 * * * *" cron: "*/10 * * * *"
# runs the synchronisation on startup
runOnStart: true
origin: origin:
# url of the origin instance # url of the origin instance
url: https://192.168.1.2:3000 url: https://192.168.1.2:3000
@@ -128,6 +142,7 @@ replicas:
- url: http://192.168.1.4 - url: http://192.168.1.4
username: username username: username
password: password password: password
# autoSetup: true # if true, AdGuardHome is automatically initialized.
# Configure the sync API server, disabled if api port is 0 # Configure the sync API server, disabled if api port is 0
api: api:
@@ -138,3 +153,14 @@ api:
password: password password: password
``` ```
## Log Level
The log level can be set with the environment variable: LOG_LEVEL
The following log levels are supported (default: info)
- debug
- info
- warn
- error

View File

@@ -8,13 +8,14 @@ import (
"github.com/bakito/adguardhome-sync/pkg/log" "github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types" "github.com/bakito/adguardhome-sync/pkg/types"
homedir "github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const ( const (
configCron = "cron" configCron = "cron"
configRunOnStart = "runOnStart"
configAPIPort = "api.port" configAPIPort = "api.port"
configAPIUsername = "api.username" configAPIUsername = "api.username"
@@ -31,11 +32,13 @@ const (
configReplicaUsername = "replica.username" configReplicaUsername = "replica.username"
configReplicaPassword = "replica.password" configReplicaPassword = "replica.password"
configReplicaInsecureSkipVerify = "replica.insecureSkipVerify" configReplicaInsecureSkipVerify = "replica.insecureSkipVerify"
configReplicaAutoSetup = "replica.autoSetup"
envReplicasUsernameFormat = "REPLICA%s_USERNAME" envReplicasUsernameFormat = "REPLICA%s_USERNAME"
envReplicasPasswordFormat = "REPLICA%s_PASSWORD" envReplicasPasswordFormat = "REPLICA%s_PASSWORD"
envReplicasAPIPathFormat = "REPLICA%s_APIPATH" envReplicasAPIPathFormat = "REPLICA%s_APIPATH"
envReplicasInsecureSkipVerifyFormat = "REPLICA%s_INSECURESKIPVERIFY" envReplicasInsecureSkipVerifyFormat = "REPLICA%s_INSECURESKIPVERIFY"
envReplicasAutoSetup = "REPLICA%s_AUTOSETUP"
) )
var ( var (
@@ -126,6 +129,7 @@ func collectEnvReplicas() []types.AdGuardInstance {
Password: os.Getenv(fmt.Sprintf(envReplicasPasswordFormat, sm[1])), Password: os.Getenv(fmt.Sprintf(envReplicasPasswordFormat, sm[1])),
APIPath: os.Getenv(fmt.Sprintf(envReplicasAPIPathFormat, sm[1])), APIPath: os.Getenv(fmt.Sprintf(envReplicasAPIPathFormat, sm[1])),
InsecureSkipVerify: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasInsecureSkipVerifyFormat, sm[1])), "true"), InsecureSkipVerify: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasInsecureSkipVerifyFormat, sm[1])), "true"),
AutoSetup: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasAutoSetup, sm[1])), "true"),
} }
replicas = append(replicas, re) replicas = append(replicas, re)
} }

View File

@@ -28,6 +28,8 @@ func init() {
rootCmd.AddCommand(doCmd) rootCmd.AddCommand(doCmd)
doCmd.PersistentFlags().String("cron", "", "The cron expression to run in daemon mode") doCmd.PersistentFlags().String("cron", "", "The cron expression to run in daemon mode")
_ = viper.BindPFlag(configCron, doCmd.PersistentFlags().Lookup("cron")) _ = viper.BindPFlag(configCron, doCmd.PersistentFlags().Lookup("cron"))
doCmd.PersistentFlags().Bool("runOnStart", true, "Run the sync job on start.")
_ = viper.BindPFlag(configRunOnStart, doCmd.PersistentFlags().Lookup("runOnStart"))
doCmd.PersistentFlags().Int("api-port", 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.") doCmd.PersistentFlags().Int("api-port", 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.")
_ = viper.BindPFlag(configAPIPort, doCmd.PersistentFlags().Lookup("api-port")) _ = viper.BindPFlag(configAPIPort, doCmd.PersistentFlags().Lookup("api-port"))
doCmd.PersistentFlags().String("api-username", "", "Sync API username") doCmd.PersistentFlags().String("api-username", "", "Sync API username")
@@ -54,6 +56,8 @@ func init() {
_ = viper.BindPFlag(configReplicaUsername, doCmd.PersistentFlags().Lookup("replica-username")) _ = viper.BindPFlag(configReplicaUsername, doCmd.PersistentFlags().Lookup("replica-username"))
doCmd.PersistentFlags().String("replica-password", "", "Replica instance password") doCmd.PersistentFlags().String("replica-password", "", "Replica instance password")
_ = viper.BindPFlag(configReplicaPassword, doCmd.PersistentFlags().Lookup("replica-password")) _ = viper.BindPFlag(configReplicaPassword, doCmd.PersistentFlags().Lookup("replica-password"))
doCmd.PersistentFlags().String("replica-insecure-skip-verify", "", "Enable Replica instance InsecureSkipVerify") doCmd.PersistentFlags().Bool("replica-insecure-skip-verify", false, "Enable Replica instance InsecureSkipVerify")
_ = viper.BindPFlag(configReplicaInsecureSkipVerify, doCmd.PersistentFlags().Lookup("replica-insecure-skip-verify")) _ = viper.BindPFlag(configReplicaInsecureSkipVerify, doCmd.PersistentFlags().Lookup("replica-insecure-skip-verify"))
doCmd.PersistentFlags().Bool("replica-auto-setup", false, "Enable automatic setup of new AdguardHome instances. This replaces the setup wizard.")
_ = viper.BindPFlag(configReplicaAutoSetup, doCmd.PersistentFlags().Lookup("replica-auto-setup"))
} }

6
go.mod
View File

@@ -3,14 +3,14 @@ module github.com/bakito/adguardhome-sync
go 1.16 go 1.16
require ( require (
github.com/go-resty/resty/v2 v2.4.0 github.com/go-resty/resty/v2 v2.6.0
github.com/golang/mock v1.5.0
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/onsi/ginkgo v1.16.0 github.com/onsi/ginkgo v1.16.1
github.com/onsi/gomega v1.11.0 github.com/onsi/gomega v1.11.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.1 github.com/spf13/viper v1.7.1
go.uber.org/zap v1.16.0 go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
) )

18
go.sum
View File

@@ -46,8 +46,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -57,6 +57,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -152,8 +154,8 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.0 h1:NBrNLB37exjJLxXtFOktx6CISBdS1aF8+7MwKlTV8U4= github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
github.com/onsi/ginkgo v1.16.0/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
@@ -277,9 +279,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -309,8 +310,9 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -15,12 +15,12 @@ import (
) )
var ( var (
l = log.GetLogger("client") l = log.GetLogger("client")
SetupNeededError = errors.New("setup needed")
) )
// New create a new client // New create a new client
func New(config types.AdGuardInstance) (Client, error) { func New(config types.AdGuardInstance) (Client, error) {
var apiURL string var apiURL string
if config.APIPath == "" { if config.APIPath == "" {
apiURL = fmt.Sprintf("%s/control", config.URL) apiURL = fmt.Sprintf("%s/control", config.URL)
@@ -42,6 +42,9 @@ func New(config types.AdGuardInstance) (Client, error) {
cl = cl.SetBasicAuth(config.Username, config.Password) cl = cl.SetBasicAuth(config.Username, config.Password)
} }
// no redirect
cl.SetRedirectPolicy(resty.NoRedirectPolicy())
return &client{ return &client{
host: u.Host, host: u.Host,
client: cl, client: cl,
@@ -49,7 +52,7 @@ func New(config types.AdGuardInstance) (Client, error) {
}, nil }, nil
} }
// Client AdGuard Home API client interface // Client AdguardHome API client interface
type Client interface { type Client interface {
Host() string Host() string
@@ -74,7 +77,7 @@ type Client interface {
SafeSearch() (bool, error) SafeSearch() (bool, error)
ToggleSafeSearch(enable bool) error ToggleSafeSearch(enable bool) error
Services() (*types.Services, error) Services() (types.Services, error)
SetServices(services types.Services) error SetServices(services types.Services) error
Clients() (*types.Clients, error) Clients() (*types.Clients, error)
@@ -86,6 +89,7 @@ type Client interface {
SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error
StatsConfig() (*types.IntervalConfig, error) StatsConfig() (*types.IntervalConfig, error)
SetStatsConfig(interval int) error SetStatsConfig(interval int) error
Setup() error
} }
type client struct { type client struct {
@@ -99,10 +103,22 @@ func (cl *client) Host() string {
} }
func (cl *client) doGet(req *resty.Request, url string) error { func (cl *client) doGet(req *resty.Request, url string) error {
rl := cl.log.With("method", "GET", "path", url)
if cl.client.UserInfo != nil {
rl = rl.With("username", cl.client.UserInfo.Username)
}
rl.Debug("do get")
resp, err := req.Get(url) resp, err := req.Get(url)
if err != nil { if err != nil {
if resp != nil && resp.StatusCode() == http.StatusFound {
loc := resp.Header().Get("Location")
if loc == "/install.html" {
return SetupNeededError
}
}
return err return err
} }
rl.With("status", resp.StatusCode(), "body", string(resp.Body())).Debug("got response")
if resp.StatusCode() != http.StatusOK { if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status()) return errors.New(resp.Status())
} }
@@ -110,10 +126,16 @@ func (cl *client) doGet(req *resty.Request, url string) error {
} }
func (cl *client) doPost(req *resty.Request, url string) error { func (cl *client) doPost(req *resty.Request, url string) error {
rl := cl.log.With("method", "POST", "path", url)
if cl.client.UserInfo != nil {
rl = rl.With("username", cl.client.UserInfo.Username)
}
rl.Debug("do post")
resp, err := req.Post(url) resp, err := req.Post(url)
if err != nil { if err != nil {
return err return err
} }
rl.With("status", resp.StatusCode(), "body", string(resp.Body())).Debug("got response")
if resp.StatusCode() != http.StatusOK { if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status()) return errors.New(resp.Status())
} }
@@ -124,7 +146,6 @@ func (cl *client) Status() (*types.Status, error) {
status := &types.Status{} status := &types.Status{}
err := cl.doGet(cl.client.R().EnableTrace().SetResult(status), "status") err := cl.doGet(cl.client.R().EnableTrace().SetResult(status), "status")
return status, err return status, err
} }
func (cl *client) RewriteList() (*types.RewriteEntries, error) { func (cl *client) RewriteList() (*types.RewriteEntries, error) {
@@ -204,7 +225,7 @@ func (cl *client) Filtering() (*types.FilteringStatus, error) {
func (cl *client) AddFilters(whitelist bool, filters ...types.Filter) error { func (cl *client) AddFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters { for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist).Info("Add filter") cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Add filter")
ff := &types.Filter{Name: f.Name, URL: f.URL, Whitelist: whitelist} ff := &types.Filter{Name: f.Name, URL: f.URL, Whitelist: whitelist}
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/add_url") err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/add_url")
if err != nil { if err != nil {
@@ -216,7 +237,7 @@ func (cl *client) AddFilters(whitelist bool, filters ...types.Filter) error {
func (cl *client) DeleteFilters(whitelist bool, filters ...types.Filter) error { func (cl *client) DeleteFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters { for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist).Info("Delete filter") cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Delete filter")
ff := &types.Filter{URL: f.URL, Whitelist: whitelist} ff := &types.Filter{URL: f.URL, Whitelist: whitelist}
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/remove_url") err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/remove_url")
if err != nil { if err != nil {
@@ -228,7 +249,7 @@ func (cl *client) DeleteFilters(whitelist bool, filters ...types.Filter) error {
func (cl *client) UpdateFilters(whitelist bool, filters ...types.Filter) error { func (cl *client) UpdateFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters { for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist).Info("Update filter") cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Update filter")
fu := &types.FilterUpdate{Whitelist: whitelist, URL: f.URL, Data: types.Filter{ID: f.ID, Name: f.Name, URL: f.URL, Whitelist: whitelist, Enabled: f.Enabled}} fu := &types.FilterUpdate{Whitelist: whitelist, URL: f.URL, Data: types.Filter{ID: f.ID, Name: f.Name, URL: f.URL, Whitelist: whitelist, Enabled: f.Enabled}}
err := cl.doPost(cl.client.R().EnableTrace().SetBody(fu), "/filtering/set_url") err := cl.doPost(cl.client.R().EnableTrace().SetBody(fu), "/filtering/set_url")
if err != nil { if err != nil {
@@ -261,9 +282,9 @@ func (cl *client) ToggleFiltering(enabled bool, interval int) error {
}), "/filtering/config") }), "/filtering/config")
} }
func (cl *client) Services() (*types.Services, error) { func (cl *client) Services() (types.Services, error) {
svcs := &types.Services{} svcs := types.Services{}
err := cl.doGet(cl.client.R().EnableTrace().SetResult(svcs), "/blocked_services/list") err := cl.doGet(cl.client.R().EnableTrace().SetResult(&svcs), "/blocked_services/list")
return svcs, err return svcs, err
} }
@@ -336,3 +357,29 @@ func (cl *client) SetStatsConfig(interval int) error {
cl.log.With("interval", interval).Info("Set stats config") cl.log.With("interval", interval).Info("Set stats config")
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.IntervalConfig{Interval: interval}), "/stats_config") return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.IntervalConfig{Interval: interval}), "/stats_config")
} }
func (cl *client) Setup() error {
cl.log.Info("Setup new AdguardHome instance")
cfg := &types.InstallConfig{
Web: types.InstallPort{
IP: "0.0.0.0",
Port: 3000,
Status: "",
CanAutofix: false,
},
DNS: types.InstallPort{
IP: "0.0.0.0",
Port: 53,
Status: "",
CanAutofix: false,
},
}
if cl.client.UserInfo != nil {
cfg.Username = cl.client.UserInfo.Username
cfg.Password = cl.client.UserInfo.Password
}
req := cl.client.R().EnableTrace().SetBody(cfg)
req.UserInfo = nil
return cl.doPost(req, "/install/configure")
}

View File

@@ -0,0 +1,13 @@
package client_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestClient(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Client Suite")
}

349
pkg/client/client_test.go Normal file
View File

@@ -0,0 +1,349 @@
package client_test
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/google/uuid"
)
var (
username = uuid.NewString()
password = uuid.NewString()
)
var _ = Describe("Client", func() {
var (
cl client.Client
ts *httptest.Server
)
AfterEach(func() {
if ts != nil {
ts.Close()
}
})
Context("Host", func() {
It("should read the current host", func() {
cl, _ := client.New(types.AdGuardInstance{URL: "https://foo.bar:3000"})
host := cl.Host()
Ω(host).Should(Equal("foo.bar:3000"))
})
})
Context("Filtering", func() {
It("should read filtering status", func() {
ts, cl = ClientGet("filtering-status.json", "/filtering/status")
fs, err := cl.Filtering()
Ω(err).ShouldNot(HaveOccurred())
Ω(fs.Enabled).Should(BeTrue())
Ω(fs.Filters).Should(HaveLen(2))
})
It("should enable protection", func() {
ts, cl = ClientPost("/filtering/config", `{"enabled":true,"interval":123}`)
err := cl.ToggleFiltering(true, 123)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable protection", func() {
ts, cl = ClientPost("/filtering/config", `{"enabled":false,"interval":123}`)
err := cl.ToggleFiltering(false, 123)
Ω(err).ShouldNot(HaveOccurred())
})
It("should call RefreshFilters", func() {
ts, cl = ClientPost("/filtering/refresh", `{"whitelist":true}`)
err := cl.RefreshFilters(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add Filters", func() {
ts, cl = ClientPost("/filtering/add_url",
`{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true}`,
`{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true}`,
)
err := cl.AddFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should update Filters", func() {
ts, cl = ClientPost("/filtering/set_url",
`{"url":"foo","data":{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true},"whitelist":true}`,
`{"url":"bar","data":{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true},"whitelist":true}`,
)
err := cl.UpdateFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete Filters", func() {
ts, cl = ClientPost("/filtering/remove_url",
`{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true}`,
`{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true}`,
)
err := cl.DeleteFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("CustomRules", func() {
It("should set SetCustomRules", func() {
ts, cl = ClientPost("/filtering/set_rules", `foo
bar`)
err := cl.SetCustomRules([]string{"foo", "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Status", func() {
It("should read status", func() {
ts, cl = ClientGet("status.json", "/status")
fs, err := cl.Status()
Ω(err).ShouldNot(HaveOccurred())
Ω(fs.DNSAddresses).Should(HaveLen(1))
Ω(fs.DNSAddresses[0]).Should(Equal("192.168.1.2"))
Ω(fs.Version).Should(Equal("v0.105.2"))
})
It("should return SetupNeededError", func() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/install.html")
w.WriteHeader(http.StatusFound)
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
_, err = cl.Status()
Ω(err).Should(HaveOccurred())
Ω(err).Should(Equal(client.SetupNeededError))
})
})
Context("Setup", func() {
It("should add setup the instance", func() {
ts, cl = ClientPost("/install/configure", fmt.Sprintf(`{"web":{"ip":"0.0.0.0","port":3000,"status":"","can_autofix":false},"dns":{"ip":"0.0.0.0","port":53,"status":"","can_autofix":false},"username":"%s","password":"%s"}`, username, password))
err := cl.Setup()
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("RewriteList", func() {
It("should read RewriteList", func() {
ts, cl = ClientGet("rewrite-list.json", "/rewrite/list")
rwl, err := cl.RewriteList()
Ω(err).ShouldNot(HaveOccurred())
Ω(*rwl).Should(HaveLen(2))
})
It("should add RewriteList", func() {
ts, cl = ClientPost("/rewrite/add", `{"domain":"foo","answer":"foo"}`, `{"domain":"bar","answer":"bar"}`)
err := cl.AddRewriteEntries(types.RewriteEntry{Answer: "foo", Domain: "foo"}, types.RewriteEntry{Answer: "bar", Domain: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete RewriteList", func() {
ts, cl = ClientPost("/rewrite/delete", `{"domain":"foo","answer":"foo"}`, `{"domain":"bar","answer":"bar"}`)
err := cl.DeleteRewriteEntries(types.RewriteEntry{Answer: "foo", Domain: "foo"}, types.RewriteEntry{Answer: "bar", Domain: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("SafeBrowsing", func() {
It("should read safebrowsing status", func() {
ts, cl = ClientGet("safebrowsing-status.json", "/safebrowsing/status")
sb, err := cl.SafeBrowsing()
Ω(err).ShouldNot(HaveOccurred())
Ω(sb).Should(BeTrue())
})
It("should enable safebrowsing", func() {
ts, cl = ClientPost("/safebrowsing/enable", "")
err := cl.ToggleSafeBrowsing(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable safebrowsing", func() {
ts, cl = ClientPost("/safebrowsing/disable", "")
err := cl.ToggleSafeBrowsing(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("SafeSearch", func() {
It("should read safesearch status", func() {
ts, cl = ClientGet("safesearch-status.json", "/safesearch/status")
ss, err := cl.SafeSearch()
Ω(err).ShouldNot(HaveOccurred())
Ω(ss).Should(BeTrue())
})
It("should enable safesearch", func() {
ts, cl = ClientPost("/safesearch/enable", "")
err := cl.ToggleSafeSearch(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable safesearch", func() {
ts, cl = ClientPost("/safesearch/disable", "")
err := cl.ToggleSafeSearch(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Parental", func() {
It("should read parental status", func() {
ts, cl = ClientGet("parental-status.json", "/parental/status")
p, err := cl.Parental()
Ω(err).ShouldNot(HaveOccurred())
Ω(p).Should(BeTrue())
})
It("should enable parental", func() {
ts, cl = ClientPost("/parental/enable", "")
err := cl.ToggleParental(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable parental", func() {
ts, cl = ClientPost("/parental/disable", "")
err := cl.ToggleParental(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Protection", func() {
It("should enable protection", func() {
ts, cl = ClientPost("/dns_config", `{"protection_enabled":true}`)
err := cl.ToggleProtection(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable protection", func() {
ts, cl = ClientPost("/dns_config", `{"protection_enabled":false}`)
err := cl.ToggleProtection(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Services", func() {
It("should read Services", func() {
ts, cl = ClientGet("blockedservices-list.json", "/blocked_services/list")
s, err := cl.Services()
Ω(err).ShouldNot(HaveOccurred())
Ω(s).Should(HaveLen(2))
})
It("should set Services", func() {
ts, cl = ClientPost("/blocked_services/set", `["foo","bar"]`)
err := cl.SetServices([]string{"foo", "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Clients", func() {
It("should read Clients", func() {
ts, cl = ClientGet("clients.json", "/clients")
c, err := cl.Clients()
Ω(err).ShouldNot(HaveOccurred())
Ω(c.Clients).Should(HaveLen(2))
})
It("should add Clients", func() {
ts, cl = ClientPost("/clients/add",
`{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}`,
)
err := cl.AddClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
It("should update Clients", func() {
ts, cl = ClientPost("/clients/update",
`{"name":"foo","data":{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}}`,
)
err := cl.UpdateClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete Clients", func() {
ts, cl = ClientPost("/clients/delete",
`{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}`,
)
err := cl.DeleteClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("QueryLogConfig", func() {
It("should read QueryLogConfig", func() {
ts, cl = ClientGet("querylog_info.json", "/querylog_info")
qlc, err := cl.QueryLogConfig()
Ω(err).ShouldNot(HaveOccurred())
Ω(qlc.Enabled).Should(BeTrue())
Ω(qlc.Interval).Should(Equal(90))
})
It("should set QueryLogConfig", func() {
ts, cl = ClientPost("/querylog_config", `{"enabled":true,"interval":123,"anonymize_client_ip":true}`)
err := cl.SetQueryLogConfig(true, 123, true)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("StatsConfig", func() {
It("should read StatsConfig", func() {
ts, cl = ClientGet("stats_info.json", "/stats_info")
sc, err := cl.StatsConfig()
Ω(err).ShouldNot(HaveOccurred())
Ω(sc.Interval).Should(Equal(1))
})
It("should set StatsConfig", func() {
ts, cl = ClientPost("/stats_config", `{"interval":123}`)
err := cl.SetStatsConfig(123)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("helper functions", func() {
var (
cl client.Client
)
BeforeEach(func() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
var err error
cl, err = client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
})
Context("doGet", func() {
It("should return an error on status code != 200", func() {
_, err := cl.Status()
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(Equal("401 Unauthorized"))
})
})
Context("doPost", func() {
It("should return an error on status code != 200", func() {
err := cl.SetStatsConfig(123)
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(Equal("401 Unauthorized"))
})
})
})
})
func ClientGet(file string, path string) (*httptest.Server, client.Client) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ω(r.URL.Path).Should(Equal(types.DefaultAPIPath + path))
b, err := ioutil.ReadFile(filepath.Join("../../testdata", file))
Ω(err).ShouldNot(HaveOccurred())
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
Ω(err).ShouldNot(HaveOccurred())
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
return ts, cl
}
func ClientPost(path string, content ...string) (*httptest.Server, client.Client) {
index := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ω(r.URL.Path).Should(Equal(types.DefaultAPIPath + path))
body, err := ioutil.ReadAll(r.Body)
Ω(err).ShouldNot(HaveOccurred())
Ω(body).Should(Equal([]byte(content[index])))
index++
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL, Username: username, Password: password})
Ω(err).ShouldNot(HaveOccurred())
return ts, cl
}

View File

@@ -1,11 +1,16 @@
package log package log
import ( import (
"os"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
const logHistorySize = 50 const (
logHistorySize = 50
envLogLevel = "LOG_LEVEL"
)
var ( var (
rootLogger *zap.Logger rootLogger *zap.Logger
@@ -20,6 +25,12 @@ func GetLogger(name string) *zap.SugaredLogger {
func init() { func init() {
level := zap.InfoLevel level := zap.InfoLevel
if lvl, ok := os.LookupEnv(envLogLevel); ok {
if err := level.Set(lvl); err != nil {
panic(err)
}
}
cfg := zap.Config{ cfg := zap.Config{
Level: zap.NewAtomicLevelAt(level), Level: zap.NewAtomicLevelAt(level),
Development: false, Development: false,

500
pkg/mocks/client/mock.go Normal file
View File

@@ -0,0 +1,500 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/bakito/adguardhome-sync/pkg/client (interfaces: Client)
// Package mock_client is a generated GoMock package.
package mock_client
import (
reflect "reflect"
types "github.com/bakito/adguardhome-sync/pkg/types"
gomock "github.com/golang/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// AddClients mocks base method.
func (m *MockClient) AddClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddClients indicates an expected call of AddClients.
func (mr *MockClientMockRecorder) AddClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClients", reflect.TypeOf((*MockClient)(nil).AddClients), arg0...)
}
// AddFilters mocks base method.
func (m *MockClient) AddFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddFilters indicates an expected call of AddFilters.
func (mr *MockClientMockRecorder) AddFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFilters", reflect.TypeOf((*MockClient)(nil).AddFilters), varargs...)
}
// AddRewriteEntries mocks base method.
func (m *MockClient) AddRewriteEntries(arg0 ...types.RewriteEntry) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddRewriteEntries", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddRewriteEntries indicates an expected call of AddRewriteEntries.
func (mr *MockClientMockRecorder) AddRewriteEntries(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRewriteEntries", reflect.TypeOf((*MockClient)(nil).AddRewriteEntries), arg0...)
}
// Clients mocks base method.
func (m *MockClient) Clients() (*types.Clients, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Clients")
ret0, _ := ret[0].(*types.Clients)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Clients indicates an expected call of Clients.
func (mr *MockClientMockRecorder) Clients() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clients", reflect.TypeOf((*MockClient)(nil).Clients))
}
// DeleteClients mocks base method.
func (m *MockClient) DeleteClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteClients indicates an expected call of DeleteClients.
func (mr *MockClientMockRecorder) DeleteClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteClients", reflect.TypeOf((*MockClient)(nil).DeleteClients), arg0...)
}
// DeleteFilters mocks base method.
func (m *MockClient) DeleteFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteFilters indicates an expected call of DeleteFilters.
func (mr *MockClientMockRecorder) DeleteFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFilters", reflect.TypeOf((*MockClient)(nil).DeleteFilters), varargs...)
}
// DeleteRewriteEntries mocks base method.
func (m *MockClient) DeleteRewriteEntries(arg0 ...types.RewriteEntry) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteRewriteEntries", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteRewriteEntries indicates an expected call of DeleteRewriteEntries.
func (mr *MockClientMockRecorder) DeleteRewriteEntries(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRewriteEntries", reflect.TypeOf((*MockClient)(nil).DeleteRewriteEntries), arg0...)
}
// Filtering mocks base method.
func (m *MockClient) Filtering() (*types.FilteringStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Filtering")
ret0, _ := ret[0].(*types.FilteringStatus)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Filtering indicates an expected call of Filtering.
func (mr *MockClientMockRecorder) Filtering() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filtering", reflect.TypeOf((*MockClient)(nil).Filtering))
}
// Host mocks base method.
func (m *MockClient) Host() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Host")
ret0, _ := ret[0].(string)
return ret0
}
// Host indicates an expected call of Host.
func (mr *MockClientMockRecorder) Host() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockClient)(nil).Host))
}
// Parental mocks base method.
func (m *MockClient) Parental() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Parental")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Parental indicates an expected call of Parental.
func (mr *MockClientMockRecorder) Parental() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parental", reflect.TypeOf((*MockClient)(nil).Parental))
}
// QueryLogConfig mocks base method.
func (m *MockClient) QueryLogConfig() (*types.QueryLogConfig, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryLogConfig")
ret0, _ := ret[0].(*types.QueryLogConfig)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryLogConfig indicates an expected call of QueryLogConfig.
func (mr *MockClientMockRecorder) QueryLogConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLogConfig", reflect.TypeOf((*MockClient)(nil).QueryLogConfig))
}
// RefreshFilters mocks base method.
func (m *MockClient) RefreshFilters(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RefreshFilters", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// RefreshFilters indicates an expected call of RefreshFilters.
func (mr *MockClientMockRecorder) RefreshFilters(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshFilters", reflect.TypeOf((*MockClient)(nil).RefreshFilters), arg0)
}
// RewriteList mocks base method.
func (m *MockClient) RewriteList() (*types.RewriteEntries, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RewriteList")
ret0, _ := ret[0].(*types.RewriteEntries)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RewriteList indicates an expected call of RewriteList.
func (mr *MockClientMockRecorder) RewriteList() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RewriteList", reflect.TypeOf((*MockClient)(nil).RewriteList))
}
// SafeBrowsing mocks base method.
func (m *MockClient) SafeBrowsing() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeBrowsing")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SafeBrowsing indicates an expected call of SafeBrowsing.
func (mr *MockClientMockRecorder) SafeBrowsing() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeBrowsing", reflect.TypeOf((*MockClient)(nil).SafeBrowsing))
}
// SafeSearch mocks base method.
func (m *MockClient) SafeSearch() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeSearch")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SafeSearch indicates an expected call of SafeSearch.
func (mr *MockClientMockRecorder) SafeSearch() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeSearch", reflect.TypeOf((*MockClient)(nil).SafeSearch))
}
// Services mocks base method.
func (m *MockClient) Services() (types.Services, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Services")
ret0, _ := ret[0].(types.Services)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Services indicates an expected call of Services.
func (mr *MockClientMockRecorder) Services() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Services", reflect.TypeOf((*MockClient)(nil).Services))
}
// SetCustomRules mocks base method.
func (m *MockClient) SetCustomRules(arg0 types.UserRules) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetCustomRules", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetCustomRules indicates an expected call of SetCustomRules.
func (mr *MockClientMockRecorder) SetCustomRules(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCustomRules", reflect.TypeOf((*MockClient)(nil).SetCustomRules), arg0)
}
// SetQueryLogConfig mocks base method.
func (m *MockClient) SetQueryLogConfig(arg0 bool, arg1 int, arg2 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetQueryLogConfig", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SetQueryLogConfig indicates an expected call of SetQueryLogConfig.
func (mr *MockClientMockRecorder) SetQueryLogConfig(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetQueryLogConfig", reflect.TypeOf((*MockClient)(nil).SetQueryLogConfig), arg0, arg1, arg2)
}
// SetServices mocks base method.
func (m *MockClient) SetServices(arg0 types.Services) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetServices", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetServices indicates an expected call of SetServices.
func (mr *MockClientMockRecorder) SetServices(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServices", reflect.TypeOf((*MockClient)(nil).SetServices), arg0)
}
// SetStatsConfig mocks base method.
func (m *MockClient) SetStatsConfig(arg0 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetStatsConfig", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetStatsConfig indicates an expected call of SetStatsConfig.
func (mr *MockClientMockRecorder) SetStatsConfig(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), arg0)
}
// Setup mocks base method.
func (m *MockClient) Setup() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Setup")
ret0, _ := ret[0].(error)
return ret0
}
// Setup indicates an expected call of Setup.
func (mr *MockClientMockRecorder) Setup() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setup", reflect.TypeOf((*MockClient)(nil).Setup))
}
// StatsConfig mocks base method.
func (m *MockClient) StatsConfig() (*types.IntervalConfig, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StatsConfig")
ret0, _ := ret[0].(*types.IntervalConfig)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StatsConfig indicates an expected call of StatsConfig.
func (mr *MockClientMockRecorder) StatsConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatsConfig", reflect.TypeOf((*MockClient)(nil).StatsConfig))
}
// Status mocks base method.
func (m *MockClient) Status() (*types.Status, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Status")
ret0, _ := ret[0].(*types.Status)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Status indicates an expected call of Status.
func (mr *MockClientMockRecorder) Status() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status))
}
// ToggleFiltering mocks base method.
func (m *MockClient) ToggleFiltering(arg0 bool, arg1 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleFiltering", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleFiltering indicates an expected call of ToggleFiltering.
func (mr *MockClientMockRecorder) ToggleFiltering(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleFiltering", reflect.TypeOf((*MockClient)(nil).ToggleFiltering), arg0, arg1)
}
// ToggleParental mocks base method.
func (m *MockClient) ToggleParental(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleParental", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleParental indicates an expected call of ToggleParental.
func (mr *MockClientMockRecorder) ToggleParental(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleParental", reflect.TypeOf((*MockClient)(nil).ToggleParental), arg0)
}
// ToggleProtection mocks base method.
func (m *MockClient) ToggleProtection(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleProtection", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleProtection indicates an expected call of ToggleProtection.
func (mr *MockClientMockRecorder) ToggleProtection(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleProtection", reflect.TypeOf((*MockClient)(nil).ToggleProtection), arg0)
}
// ToggleSafeBrowsing mocks base method.
func (m *MockClient) ToggleSafeBrowsing(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleSafeBrowsing", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleSafeBrowsing indicates an expected call of ToggleSafeBrowsing.
func (mr *MockClientMockRecorder) ToggleSafeBrowsing(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeBrowsing", reflect.TypeOf((*MockClient)(nil).ToggleSafeBrowsing), arg0)
}
// ToggleSafeSearch mocks base method.
func (m *MockClient) ToggleSafeSearch(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleSafeSearch", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleSafeSearch indicates an expected call of ToggleSafeSearch.
func (mr *MockClientMockRecorder) ToggleSafeSearch(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeSearch", reflect.TypeOf((*MockClient)(nil).ToggleSafeSearch), arg0)
}
// UpdateClients mocks base method.
func (m *MockClient) UpdateClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateClients indicates an expected call of UpdateClients.
func (mr *MockClientMockRecorder) UpdateClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClients", reflect.TypeOf((*MockClient)(nil).UpdateClients), arg0...)
}
// UpdateFilters mocks base method.
func (m *MockClient) UpdateFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateFilters indicates an expected call of UpdateFilters.
func (mr *MockClientMockRecorder) UpdateFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFilters", reflect.TypeOf((*MockClient)(nil).UpdateFilters), varargs...)
}

View File

@@ -1,6 +1,7 @@
package sync package sync
import ( import (
"errors"
"fmt" "fmt"
"github.com/bakito/adguardhome-sync/pkg/client" "github.com/bakito/adguardhome-sync/pkg/client"
@@ -26,8 +27,13 @@ func Sync(cfg *types.Config) error {
return fmt.Errorf("no replicas configured") return fmt.Errorf("no replicas configured")
} }
cfg.Origin.AutoSetup = false
w := &worker{ w := &worker{
cfg: cfg, cfg: cfg,
createClient: func(ai types.AdGuardInstance) (client.Client, error) {
return client.New(ai)
},
} }
if cfg.Cron != "" { if cfg.Cron != "" {
w.cron = cron.New() w.cron = cron.New()
@@ -45,7 +51,9 @@ func Sync(cfg *types.Config) error {
} else { } else {
w.cron.Run() w.cron.Run()
} }
} else { }
if cfg.RunOnStart {
l.With("version", version.Version).Info("Run on startup")
w.sync() w.sync()
} }
if cfg.API.Port != 0 { if cfg.API.Port != 0 {
@@ -55,9 +63,10 @@ func Sync(cfg *types.Config) error {
} }
type worker struct { type worker struct {
cfg *types.Config cfg *types.Config
running bool running bool
cron *cron.Cron cron *cron.Cron
createClient func(instance types.AdGuardInstance) (client.Client, error)
} }
func (w *worker) sync() { func (w *worker) sync() {
@@ -68,7 +77,7 @@ func (w *worker) sync() {
w.running = true w.running = true
defer func() { w.running = false }() defer func() { w.running = false }()
oc, err := client.New(w.cfg.Origin) oc, err := w.createClient(w.cfg.Origin)
if err != nil { if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client") l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return return
@@ -77,7 +86,6 @@ func (w *worker) sync() {
sl := l.With("from", oc.Host()) sl := l.With("from", oc.Host())
o := &origin{} o := &origin{}
o.status, err = oc.Status() o.status, err = oc.Status()
if err != nil { if err != nil {
sl.With("error", err).Error("Error getting origin status") sl.With("error", err).Error("Error getting origin status")
@@ -141,7 +149,7 @@ func (w *worker) sync() {
func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) { func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
rc, err := client.New(replica) rc, err := w.createClient(replica)
if err != nil { if err != nil {
l.With("error", err, "url", replica.URL).Error("Error creating replica client") l.With("error", err, "url", replica.URL).Error("Error creating replica client")
return return
@@ -150,61 +158,76 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
rl := l.With("to", rc.Host()) rl := l.With("to", rc.Host())
rl.Info("Start sync") rl.Info("Start sync")
rs, err := rc.Status() rs, err := w.statusWithSetup(rl, replica, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error getting replica status") rl.With("error", err).Error("Error getting replica status")
return return
} }
if o.status.Version != rs.Version { if o.status.Version != rs.Version {
l.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match") rl.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
} }
err = w.syncGeneralSettings(o, rs, rc) err = w.syncGeneralSettings(o, rs, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error syncing general settings") rl.With("error", err).Error("Error syncing general settings")
return return
} }
err = w.syncConfigs(o, rs, rc) err = w.syncConfigs(o, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error syncing configs") rl.With("error", err).Error("Error syncing configs")
return return
} }
err = w.syncRewrites(o.rewrites, rc) err = w.syncRewrites(o.rewrites, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error syncing rewrites") rl.With("error", err).Error("Error syncing rewrites")
return return
} }
err = w.syncFilters(o.filters, rc) err = w.syncFilters(o.filters, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error syncing filters") rl.With("error", err).Error("Error syncing filters")
return return
} }
err = w.syncServices(o.services, rc) err = w.syncServices(o.services, rc)
if err != nil { if err != nil {
l.With("error", err).Error("Error syncing services") rl.With("error", err).Error("Error syncing services")
return return
} }
if err = w.syncClients(o.clients, rc); err != nil { if err = w.syncClients(o.clients, rc); err != nil {
l.With("error", err).Error("Error syncing clients") rl.With("error", err).Error("Error syncing clients")
return return
} }
rl.Info("Sync done") rl.Info("Sync done")
} }
func (w *worker) syncServices(os *types.Services, replica client.Client) error { func (w *worker) statusWithSetup(rl *zap.SugaredLogger, replica types.AdGuardInstance, rc client.Client) (*types.Status, error) {
rs, err := rc.Status()
if err != nil {
if replica.AutoSetup && errors.Is(err, client.SetupNeededError) {
if serr := rc.Setup(); serr != nil {
rl.With("error", serr).Error("Error setup AdGuardHome")
return nil, err
}
return rc.Status()
}
return nil, err
}
return rs, err
}
func (w *worker) syncServices(os types.Services, replica client.Client) error {
rs, err := replica.Services() rs, err := replica.Services()
if err != nil { if err != nil {
return err return err
} }
if !os.Equals(rs) { if !os.Equals(rs) {
if err := replica.SetServices(*os); err != nil { if err := replica.SetServices(os); err != nil {
return err return err
} }
} }
@@ -217,40 +240,10 @@ func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) e
return err return err
} }
fa, fu, fd := rf.Filters.Merge(of.Filters) if err = w.syncFilterType(of.Filters, rf.Filters, false, replica); err != nil {
if err = replica.AddFilters(false, fa...); err != nil {
return err return err
} }
if err = replica.UpdateFilters(false, fu...); err != nil { if err = w.syncFilterType(of.WhitelistFilters, rf.WhitelistFilters, true, replica); err != nil {
return err
}
if len(fa) > 0 || len(fu) > 0 {
if err = replica.RefreshFilters(false); err != nil {
return err
}
}
if err = replica.DeleteFilters(false, fd...); err != nil {
return err
}
fa, fu, fd = rf.WhitelistFilters.Merge(of.WhitelistFilters)
if err = replica.AddFilters(true, fa...); err != nil {
return err
}
if err = replica.UpdateFilters(true, fu...); err != nil {
return err
}
if len(fa) > 0 || len(fu) > 0 {
if err = replica.RefreshFilters(true); err != nil {
return err
}
}
if err = replica.DeleteFilters(true, fd...); err != nil {
return err return err
} }
@@ -266,6 +259,28 @@ func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) e
return nil return nil
} }
func (w *worker) syncFilterType(of types.Filters, rFilters types.Filters, whitelist bool, replica client.Client) error {
fa, fu, fd := rFilters.Merge(of)
if err := replica.AddFilters(whitelist, fa...); err != nil {
return err
}
if err := replica.UpdateFilters(whitelist, fu...); err != nil {
return err
}
if len(fa) > 0 || len(fu) > 0 {
if err := replica.RefreshFilters(whitelist); err != nil {
return err
}
}
if err := replica.DeleteFilters(whitelist, fd...); err != nil {
return err
}
return nil
}
func (w *worker) syncRewrites(or *types.RewriteEntries, replica client.Client) error { func (w *worker) syncRewrites(or *types.RewriteEntries, replica client.Client) error {
replicaRewrites, err := replica.RewriteList() replicaRewrites, err := replica.RewriteList()
@@ -334,7 +349,7 @@ func (w *worker) syncGeneralSettings(o *origin, rs *types.Status, replica client
return nil return nil
} }
func (w *worker) syncConfigs(o *origin, rs *types.Status, replica client.Client) error { func (w *worker) syncConfigs(o *origin, replica client.Client) error {
qlc, err := replica.QueryLogConfig() qlc, err := replica.QueryLogConfig()
if err != nil { if err != nil {
return err return err
@@ -361,7 +376,7 @@ func (w *worker) syncConfigs(o *origin, rs *types.Status, replica client.Client)
type origin struct { type origin struct {
status *types.Status status *types.Status
rewrites *types.RewriteEntries rewrites *types.RewriteEntries
services *types.Services services types.Services
filters *types.FilteringStatus filters *types.FilteringStatus
clients *types.Clients clients *types.Clients
queryLogConfig *types.QueryLogConfig queryLogConfig *types.QueryLogConfig

View File

@@ -0,0 +1,13 @@
package sync_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestSync(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Sync Suite")
}

419
pkg/sync/sync_test.go Normal file
View File

@@ -0,0 +1,419 @@
package sync
import (
"errors"
"github.com/bakito/adguardhome-sync/pkg/client"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
mc "github.com/bakito/adguardhome-sync/pkg/mocks/client"
"github.com/bakito/adguardhome-sync/pkg/types"
gm "github.com/golang/mock/gomock"
"github.com/google/uuid"
)
var _ = Describe("Sync", func() {
var (
mockCtrl *gm.Controller
cl *mc.MockClient
w *worker
te error
)
BeforeEach(func() {
mockCtrl = gm.NewController(GinkgoT())
cl = mc.NewMockClient(mockCtrl)
w = &worker{
createClient: func(instance types.AdGuardInstance) (client.Client, error) {
return cl, nil
},
}
te = errors.New(uuid.NewString())
})
AfterEach(func() {
defer mockCtrl.Finish()
})
Context("worker", func() {
Context("syncRewrites", func() {
var (
domain string
answer string
reO types.RewriteEntries
reR types.RewriteEntries
)
BeforeEach(func() {
domain = uuid.NewString()
answer = uuid.NewString()
reO = []types.RewriteEntry{{Domain: domain, Answer: answer}}
reR = []types.RewriteEntry{{Domain: domain, Answer: answer}}
})
It("should have no changes (empty slices)", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add one rewrite entry", func() {
reR = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries(reO[0])
cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should remove one rewrite entry", func() {
reO = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should remove one rewrite entry", func() {
reO = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should return error when error on RewriteList()", func() {
cl.EXPECT().RewriteList().Return(nil, te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on AddRewriteEntries()", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries().Return(te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on DeleteRewriteEntries()", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries().Return(te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
})
Context("syncClients", func() {
var (
clO *types.Clients
clR *types.Clients
name string
)
BeforeEach(func() {
name = uuid.NewString()
clO = &types.Clients{Clients: []types.Client{{Name: name}}}
clR = &types.Clients{Clients: []types.Client{{Name: name}}}
})
It("should have no changes (empty slices)", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add one client", func() {
clR.Clients = []types.Client{}
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients(clO.Clients[0])
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should update one client", func() {
clR.Clients[0].Disallowed = true
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients(clO.Clients[0])
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete one client", func() {
clO.Clients = []types.Client{}
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients(clR.Clients[0])
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should return error when error on Clients()", func() {
cl.EXPECT().Clients().Return(nil, te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on AddClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on UpdateClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on DeleteClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
})
Context("syncGeneralSettings", func() {
var (
o *origin
rs *types.Status
)
BeforeEach(func() {
o = &origin{
status: &types.Status{},
}
rs = &types.Status{}
})
It("should have no changes", func() {
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have protection enabled changes", func() {
o.status.ProtectionEnabled = true
cl.EXPECT().ToggleProtection(true)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have parental enabled changes", func() {
o.parental = true
cl.EXPECT().Parental()
cl.EXPECT().ToggleParental(true)
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have safeSearch enabled changes", func() {
o.safeSearch = true
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().ToggleSafeSearch(true)
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have safeBrowsing enabled changes", func() {
o.safeBrowsing = true
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().ToggleSafeBrowsing(true)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("syncConfigs", func() {
var (
o *origin
qlc *types.QueryLogConfig
sc *types.IntervalConfig
)
BeforeEach(func() {
o = &origin{
queryLogConfig: &types.QueryLogConfig{},
statsConfig: &types.IntervalConfig{},
}
qlc = &types.QueryLogConfig{}
sc = &types.IntervalConfig{}
})
It("should have no changes", func() {
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have QueryLogConfig changes", func() {
o.queryLogConfig.Interval = 123
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().SetQueryLogConfig(false, 123, false)
cl.EXPECT().StatsConfig().Return(sc, nil)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have StatsConfig changes", func() {
o.statsConfig.Interval = 123
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil)
cl.EXPECT().SetStatsConfig(123)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("statusWithSetup", func() {
var (
status *types.Status
inst types.AdGuardInstance
)
BeforeEach(func() {
status = &types.Status{}
inst = types.AdGuardInstance{
AutoSetup: true,
}
})
It("should get the replica status", func() {
cl.EXPECT().Status().Return(status, nil)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).ShouldNot(HaveOccurred())
Ω(st).Should(Equal(status))
})
It("should runs setup before getting replica status", func() {
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
cl.EXPECT().Setup()
cl.EXPECT().Status().Return(status, nil)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).ShouldNot(HaveOccurred())
Ω(st).Should(Equal(status))
})
It("should fail on setup", func() {
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
cl.EXPECT().Setup().Return(te)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).Should(HaveOccurred())
Ω(st).Should(BeNil())
})
})
Context("syncServices", func() {
var (
os types.Services
rs types.Services
)
BeforeEach(func() {
os = []string{"foo"}
rs = []string{"foo"}
})
It("should have no changes", func() {
cl.EXPECT().Services().Return(rs, nil)
err := w.syncServices(os, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have services changes", func() {
os = []string{"bar"}
cl.EXPECT().Services().Return(rs, nil)
cl.EXPECT().SetServices(os)
err := w.syncServices(os, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("syncFilters", func() {
var (
of *types.FilteringStatus
rf *types.FilteringStatus
)
BeforeEach(func() {
of = &types.FilteringStatus{}
rf = &types.FilteringStatus{}
})
It("should have no changes", func() {
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have changes user roles", func() {
of.UserRules = []string{"foo"}
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().SetCustomRules(of.UserRules)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have changed filtering config", func() {
of.Enabled = true
of.Interval = 123
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().ToggleFiltering(of.Enabled, of.Interval)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("sync", func() {
It("should have no changes", func() {
w.cfg = &types.Config{
Origin: types.AdGuardInstance{},
Replica: types.AdGuardInstance{URL: "foo"},
}
// origin
cl.EXPECT().Host()
cl.EXPECT().Status().Return(&types.Status{}, nil)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().RewriteList().Return(&types.RewriteEntries{}, nil)
cl.EXPECT().Services()
cl.EXPECT().Filtering().Return(&types.FilteringStatus{}, nil)
cl.EXPECT().Clients().Return(&types.Clients{}, nil)
cl.EXPECT().QueryLogConfig().Return(&types.QueryLogConfig{}, nil)
cl.EXPECT().StatsConfig().Return(&types.IntervalConfig{}, nil)
// replica
cl.EXPECT().Host()
cl.EXPECT().Status().Return(&types.Status{}, nil)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().QueryLogConfig().Return(&types.QueryLogConfig{}, nil)
cl.EXPECT().StatsConfig().Return(&types.IntervalConfig{}, nil)
cl.EXPECT().RewriteList().Return(&types.RewriteEntries{}, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries()
cl.EXPECT().Filtering().Return(&types.FilteringStatus{}, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().Services()
cl.EXPECT().Clients().Return(&types.Clients{}, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
w.sync()
})
})
})
})

View File

@@ -13,11 +13,12 @@ const (
// Config application configuration struct // Config application configuration struct
type Config struct { type Config struct {
Origin AdGuardInstance `json:"origin" yaml:"origin"` Origin AdGuardInstance `json:"origin" yaml:"origin"`
Replica AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"` Replica AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"`
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty"` Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty"` Cron string `json:"cron,omitempty" yaml:"cron,omitempty"`
API API `json:"api,omitempty" yaml:"api,omitempty"` RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty"`
API API `json:"api,omitempty" yaml:"api,omitempty"`
} }
// API configuration // API configuration
@@ -49,13 +50,14 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
return r return r
} }
// AdGuardInstance adguard home config instance // AdGuardInstance AdguardHome config instance
type AdGuardInstance struct { type AdGuardInstance struct {
URL string `json:"url" yaml:"url"` URL string `json:"url" yaml:"url"`
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"` APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"` InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
AutoSetup bool `json:"autoSetup" yaml:"autoSetup"`
} }
// Key AdGuardInstance key // Key AdGuardInstance key
@@ -122,6 +124,35 @@ func (re *RewriteEntry) Key() string {
// Filters list of Filter // Filters list of Filter
type Filters []Filter type Filters []Filter
// Merge merge Filters
func (f Filters) Merge(other Filters) (Filters, Filters, Filters) {
current := make(map[string]Filter)
var adds Filters
var updates Filters
var removes Filters
for _, f := range f {
current[f.URL] = f
}
for _, rr := range other {
if c, ok := current[rr.URL]; ok {
if !c.Equals(&rr) {
updates = append(updates, rr)
}
delete(current, rr.URL)
} else {
adds = append(adds, rr)
}
}
for _, rr := range current {
removes = append(removes, rr)
}
return adds, updates, removes
}
// Filter API struct // Filter API struct
type Filter struct { type Filter struct {
ID int `json:"id"` ID int `json:"id"`
@@ -193,35 +224,6 @@ type RefreshFilter struct {
Whitelist bool `json:"whitelist"` Whitelist bool `json:"whitelist"`
} }
// Merge merge RefreshFilters
func (fs *Filters) Merge(other Filters) (Filters, Filters, Filters) {
current := make(map[string]Filter)
var adds Filters
var updates Filters
var removes Filters
for _, f := range *fs {
current[f.URL] = f
}
for _, rr := range other {
if c, ok := current[rr.URL]; ok {
if !c.Equals(&rr) {
updates = append(updates, rr)
}
delete(current, rr.URL)
} else {
adds = append(adds, rr)
}
}
for _, rr := range current {
removes = append(removes, rr)
}
return adds, updates, removes
}
// Services API struct // Services API struct
type Services []string type Services []string
@@ -231,10 +233,10 @@ func (s Services) Sort() {
} }
// Equals Services equal check // Equals Services equal check
func (s *Services) Equals(o *Services) bool { func (s Services) Equals(o Services) bool {
s.Sort() s.Sort()
o.Sort() o.Sort()
return equals(*s, *o) return equals(s, o)
} }
// Clients API struct // Clients API struct
@@ -252,10 +254,10 @@ type Clients struct {
// Client API struct // Client API struct
type Client struct { type Client struct {
Ids []string `json:"ids"` Ids []string `json:"ids,omitempty"`
Tags []string `json:"tags"` Tags []string `json:"tags,omitempty"`
BlockedServices []string `json:"blocked_services"` BlockedServices []string `json:"blocked_services,omitempty"`
Upstreams []string `json:"upstreams"` Upstreams []string `json:"upstreams,omitempty"`
UseGlobalSettings bool `json:"use_global_settings"` UseGlobalSettings bool `json:"use_global_settings"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
@@ -337,3 +339,19 @@ func equals(a []string, b []string) bool {
} }
return true return true
} }
// InstallConfig AdguardHome install config
type InstallConfig struct {
Web InstallPort `json:"web"`
DNS InstallPort `json:"dns"`
Username string `json:"username"`
Password string `json:"password"`
}
// InstallPort AdguardHome install config port
type InstallPort struct {
IP string `json:"ip"`
Port int `json:"port"`
Status string `json:"status"`
CanAutofix bool `json:"can_autofix"`
}

View File

@@ -17,7 +17,7 @@ var _ = Describe("Types", func() {
apiPath string apiPath string
) )
BeforeEach(func() { BeforeEach(func() {
url = "http://" + uuid.NewString() url = "https://" + uuid.NewString()
apiPath = "/" + uuid.NewString() apiPath = "/" + uuid.NewString()
}) })
@@ -86,6 +86,15 @@ var _ = Describe("Types", func() {
Ω(u[0].Name).Should(Equal(name1)) Ω(u[0].Name).Should(Equal(name1))
}) })
It("should have no changes", func() {
originFilters = append(originFilters, types.Filter{URL: url})
replicaFilters = append(replicaFilters, types.Filter{URL: url})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(BeEmpty())
Ω(u).Should(BeEmpty())
Ω(d).Should(BeEmpty())
})
}) })
}) })
Context("AdGuardInstance", func() { Context("AdGuardInstance", func() {
@@ -102,6 +111,42 @@ var _ = Describe("Types", func() {
Ω(re.Key()).Should(Equal(domain + "#" + answer)) Ω(re.Key()).Should(Equal(domain + "#" + answer))
}) })
}) })
Context("QueryLogConfig", func() {
Context("Equal", func() {
var (
a *types.QueryLogConfig
b *types.QueryLogConfig
)
BeforeEach(func() {
a = &types.QueryLogConfig{}
b = &types.QueryLogConfig{}
})
It("should be equal", func() {
a.Enabled = true
a.Interval = 1
a.AnonymizeClientIP = true
b.Enabled = true
b.Interval = 1
b.AnonymizeClientIP = true
Ω(a.Equals(b)).Should(BeTrue())
})
It("should not be equal when enabled differs", func() {
a.Enabled = true
b.Enabled = false
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
It("should not be equal when interval differs", func() {
a.Interval = 1
b.Interval = 2
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
It("should not be equal when anonymizeClientIP differs", func() {
a.AnonymizeClientIP = true
b.AnonymizeClientIP = false
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
})
})
Context("RewriteEntries", func() { Context("RewriteEntries", func() {
Context("Merge", func() { Context("Merge", func() {
var ( var (
@@ -132,6 +177,14 @@ var _ = Describe("Types", func() {
Ω(d[0].Domain).Should(Equal(domain)) Ω(d[0].Domain).Should(Equal(domain))
}) })
It("should have no changes", func() {
originRE = append(originRE, types.RewriteEntry{Domain: domain})
replicaRE = append(replicaRE, types.RewriteEntry{Domain: domain})
a, d := replicaRE.Merge(&originRE)
Ω(a).Should(BeEmpty())
Ω(d).Should(BeEmpty())
})
}) })
}) })
Context("UserRules", func() { Context("UserRules", func() {
@@ -233,4 +286,23 @@ var _ = Describe("Types", func() {
}) })
}) })
}) })
Context("Services", func() {
Context("Equals", func() {
It("should be equal", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"b", "a"})
Ω(s1.Equals(s2)).Should(BeTrue())
})
It("should not be equal different values", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"B", "a"})
Ω(s1.Equals(s2)).ShouldNot(BeTrue())
})
It("should not be equal different length", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"b", "a", "c"})
Ω(s1.Equals(s2)).ShouldNot(BeTrue())
})
})
})
}) })

4
testdata/blockedservices-list.json vendored Normal file
View File

@@ -0,0 +1,4 @@
[
"9gag",
"dailymotion"
]

81
testdata/clients.json vendored Normal file
View File

@@ -0,0 +1,81 @@
{
"clients": [
{
"ids": [
"192.168.1.3"
],
"tags": [
"device_pc"
],
"name": "PC",
"use_global_settings": true,
"filtering_enabled": false,
"parental_enabled": false,
"safesearch_enabled": false,
"safebrowsing_enabled": false,
"use_global_blocked_services": true,
"blocked_services": null,
"upstreams": null,
"whois_info": null,
"disallowed": false,
"disallowed_rule": ""
},
{
"ids": [
"192.168.1.2"
],
"tags": [
"device_phone"
],
"name": "Phone LAN",
"use_global_settings": true,
"filtering_enabled": false,
"parental_enabled": false,
"safesearch_enabled": false,
"safebrowsing_enabled": false,
"use_global_blocked_services": false,
"blocked_services": [
"facebook",
"ok",
"vk",
"mail_ru",
"qq"
],
"upstreams": [],
"whois_info": null,
"disallowed": false,
"disallowed_rule": ""
}
],
"auto_clients": [
{
"ip": "127.0.0.1",
"name": "localhost",
"source": "etc/hosts",
"whois_info": {}
}
],
"supported_tags": [
"device_audio",
"device_camera",
"device_gameconsole",
"device_laptop",
"device_nas",
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_securityalarm",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_regular"
]
}

3
testdata/parental-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

5
testdata/querylog_info.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"enabled": true,
"interval": 90,
"anonymize_client_ip": false
}

10
testdata/rewrite-list.json vendored Normal file
View File

@@ -0,0 +1,10 @@
[
{
"domain": "foo.com",
"answer": "192.168.1.10"
},
{
"domain": "bar.com",
"answer": "192.168.1.12"
}
]

3
testdata/safebrowsing-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

3
testdata/safesearch-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

3
testdata/stats_info.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"interval": 1
}

12
testdata/status.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"dns_addresses": [
"192.168.1.2"
],
"dns_port": 53,
"http_port": 45158,
"protection_enabled": true,
"dhcp_available": true,
"running": true,
"version": "v0.105.2",
"language": "en"
}