diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b1f8f0..307bddc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,14 +27,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + # Lint via `make lint` — the single source of truth (pinned golangci-lint Docker + # image), byte-identical to a developer's local run. No setup-go: golangci-lint runs + # inside the container. The cache holds go modules + build/analysis cache for speed. + - name: lint cache + uses: actions/cache@v4 with: - version: v2.12.2 # pinned golangci-lint v2 — matches .golangci.yaml (version "2") + path: .lintcache + key: lint-${{ runner.os }}-${{ hashFiles('go.sum', '.golangci.yaml', 'Makefile') }} + restore-keys: lint-${{ runner.os }}- + - name: make lint + run: make lint unit: name: unit diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b2ebde3..739fba5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,10 +39,16 @@ jobs: with: go-version-file: go.mod cache-dependency-path: go.sum - - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + # Lint via `make lint` (pinned golangci-lint Docker image) — same single source of + # truth as CI and local; the cache holds go modules + build/analysis cache. + - name: lint cache + uses: actions/cache@v4 with: - version: v2.12.2 # pinned — matches .golangci.yaml (version "2") + path: .lintcache + key: lint-${{ runner.os }}-${{ hashFiles('go.sum', '.golangci.yaml', 'Makefile') }} + restore-keys: lint-${{ runner.os }}- + - name: lint (make lint) + run: make lint - name: unit tests (-race) run: make test - name: integration tests (real temp SQLite) diff --git a/.gitignore b/.gitignore index 1d2cb90..667031c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ db/ coverage.out *.coverprofile +# `make lint` Docker cache (go modules + build/analysis cache); CI persists it via cache action +.lintcache/ + # Go workspace (local, per-machine) go.work go.work.sum diff --git a/AGENTS.md b/AGENTS.md index 13c035d..69ff7bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -377,8 +377,13 @@ RENAME (иначе SQLite переписывает FK в чужих таблиц Зависимости сущности (хендлера, сервиса, …) объявляются **приватным интерфейсом в том пакете, где используются**, в файле `contract.go`, с директивой генерации мока. Интерфейс описывает ровно те методы, что нужны этому пакету (interface -segregation), и именуется по роли зависимости (`itemPlatformClient`, -`curlGenerator`, `usersRepository`). +segregation), и **именуется по конкретной зависимости, на которую ссылается** (а не по +абстрактной роли). По имени должно быть видно, что это: репозиторий → `<сущность>Repo` +(`usersRepo`, `nodesRepo`, `configsRepo`, `routingRepo`), сервис → `<сущность>Service` +(`fleetService`, `provisioningService`, `sublinksService`), клиент → `<сущность>Client` +(`panelClient`, `itemPlatformClient`). Роль-имена, по которым не видно repo/service/client +(`subLinker`, `configResolver`, `mihomoReader`, `creator`, `deleter`), **запрещены** — +они усложняют чтение. ```go // contract.go @@ -395,7 +400,7 @@ type itemPlatformClient interface { Get(ctx context.Context, in entity.ItemGetIn) (map[int64]entity.Item, error) } -type curlGenerator interface { +type curlService interface { Generate(ctx context.Context, pl entity.CURLGeneratorPayload) ([]entity.CURL, error) } ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 077034a..3c65339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ в [`AGENTS.md`](AGENTS.md) (раздел «Документирование изменений»). Версий/тегов нет: сервис не релизится, деплой непрерывный. +## 2026-06-17 — Подписка: попап со ссылками из бэкенда (raw URL + clashmi-диплинк) (#116) + +Колонка «Подписка» в списке пользователей теперь открывает попап со списком копируемых +ссылок, а не одну кнопку Mihomo: сейчас это сырой URL подписки Mihomo и диплинк +`clashmi://install-config?url=&name=&overwrite=false`. Состав ссылок и их +тайтлы целиком приходят с бэка — новый сервис `internal/service/sublinks` владеет +каталогом, фронт ничего не хардкодит (добавить движок/приложение = одна строка каталога); +в попапе показываются только тайтл и кнопка «Копировать» (значение приватное). Форма `sub` +в `GET /admin/api/users` сменилась с `{id,url}` на `{links:[{title,value}]}`; `name` +clashmi-диплинка = profile title эффективного (кастомного, иначе базового) конфига +пользователя. См. [ADR-0008](docs/decisions/0008-subscription-link-catalog.md). + +Заодно в этом PR: блок «Параметры подписки» поднят первым на вкладке «Конфиг Mihomo»; +убраны остатки githooks (таргет `make hooks` и локальный `core.hooksPath` — каталог +`.githooks/` был удалён ранее); по ревью — нейминг интерфейсов во всех `contract.go` +приведён к имени конкретной зависимости (repo → `<сущность>Repo`, service → +`<сущность>Service`, client → `<сущность>Client`) вместо ролевых имён +(`subLinker`/`configResolver`/`creator`/…); правило закреплено в `AGENTS.md`. Линтинг +сведён к единому источнику правды: `make lint` гоняет golangci-lint в pinned Docker-образе +`golangci/golangci-lint`, и **CI (`ci.yml`/`deploy.yml`) теперь вызывает тот же `make lint`** +вместо `golangci-lint-action` — локальный и CI-линт не расходятся. Платформа — нативная для +хоста (CI amd64 / Apple Silicon arm64): все включённые линтеры арх-независимы на 64-бит, так +что результат идентичный без эмуляции (максимум скорости); кэш модулей/анализа лежит в +gitignored `.lintcache/` (на CI — через `actions/cache`). Это и закрыло прежний рассинхрон +по `wsl_v5`. + ## 2026-06-16 — Логические правила mihomo (AND/OR/NOT) с рекурсивным tree-UI (#114) Маршрутное правило теперь умеет логические операторы `AND`/`OR`/`NOT` с произвольно diff --git a/Makefile b/Makefile index 4803872..047678f 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,41 @@ # subgen developer tasks. # # make generate — run go generate across the project (mockgen contracts) -# make lint — run golangci-lint (config: .golangci.yaml) +# make lint — run golangci-lint in Docker, byte-identical to CI (see below) # make test — run the unit tests (-race) # make integration — run the repository integration tests against a real temp # SQLite (-tags integration) # make apitest — run the API tests against real 3x-ui panels in docker # (delegates to apitest/Makefile: up panels → run → down) # make all — generate, then lint + test + integration + apitest -# make hooks — install the tracked git hooks (core.hooksPath = .githooks); -# the pre-push hook runs `make all` before every push # -# Lint requires golangci-lint v2: https://golangci-lint.run/welcome/install/ - -.PHONY: generate lint test integration apitest all hooks +# Lint is the SINGLE source of truth for how golangci-lint runs: CI (.github/workflows) +# invokes `make lint` too, so local and CI never diverge. It runs the pinned +# golangci/golangci-lint image on the HOST's native architecture — every enabled linter +# is AST/type-based and identical across 64-bit arches, so linux/amd64 (CI) and +# linux/arm64 (Apple Silicon) give the same result with NO emulation (max speed). Module +# downloads and the build/analysis cache persist in the gitignored .lintcache/, so repeat +# runs are fast (CI persists it with actions/cache). Needs only Docker — no local +# golangci-lint install. If your active Docker context can't bind-mount the repo, pass a +# working one: `DOCKER_CONTEXT=desktop-linux make lint`. + +.PHONY: generate lint test integration apitest all + +# Single source of truth for the golangci-lint version (CI runs `make lint`, so it is +# pinned only here). Keep in sync with .golangci.yaml's `version: "2"`. +GOLANGCI_LINT_VERSION := v2.12.2 +LINT_CACHE := $(CURDIR)/.lintcache generate: go generate ./... lint: - golangci-lint run ./... + @mkdir -p "$(LINT_CACHE)/cache" "$(LINT_CACHE)/gomod" + docker run --rm \ + -v "$(CURDIR)":/app -w /app \ + -v "$(LINT_CACHE)/cache":/root/.cache \ + -v "$(LINT_CACHE)/gomod":/go/pkg/mod \ + golangci/golangci-lint:$(GOLANGCI_LINT_VERSION) golangci-lint run ./... test: go test -race ./... @@ -31,7 +47,3 @@ apitest: $(MAKE) -C apitest test all: generate lint test integration apitest - -hooks: - git config core.hooksPath .githooks - @echo "git hooks installed (core.hooksPath = .githooks); pre-push runs 'make all'" diff --git a/apitest/api/client_users.go b/apitest/api/client_users.go index a10c01b..1360b54 100644 --- a/apitest/api/client_users.go +++ b/apitest/api/client_users.go @@ -5,6 +5,7 @@ package api import ( "fmt" "net/url" + "strings" ) // The User* read DTOs below are hand-rolled to mirror the users-list response @@ -20,11 +21,30 @@ type UserInbound struct { Missing bool `json:"missing"` } -// UserSub is a user's subscription coordinates: the shared subId and the absolute -// /sub URL (token-signed) the user fetches. +// UserSubLink is one copyable subscription link the users API reports: a display title +// and the literal value (a raw /sub URL or an app deeplink that embeds it). +type UserSubLink struct { + Title string `json:"title"` + Value string `json:"value"` +} + +// UserSub is a user's subscription presentation: the ordered, copyable links. The +// backend decides which links exist and their titles; this is a black-box mirror of that. type UserSub struct { - ID string `json:"id"` - URL string `json:"url"` + Links []UserSubLink `json:"links"` +} + +// SubURL returns the raw, token-signed /sub URL among the links — the http(s) one a +// client GETs. It is picked by scheme (deeplinks use app schemes like clashmi://), not by +// title, so the test stays agnostic to which links the backend offers. +func (s UserSub) SubURL() string { + for _, l := range s.Links { + if strings.HasPrefix(l.Value, "http") { + return l.Value + } + } + + return "" } // UserStats is a user's aggregated traffic, as the users API computes from the fleet diff --git a/apitest/sub/sub_valid_test.go b/apitest/sub/sub_valid_test.go index 0876fb0..629fb81 100644 --- a/apitest/sub/sub_valid_test.go +++ b/apitest/sub/sub_valid_test.go @@ -44,11 +44,13 @@ func (s *SubPanelSuite) TestSubValid() { u, err := s.API().MustFindUser(name) s.Require().NoError(err) s.T().Cleanup(func() { _, _ = s.API().DeleteUser(u.ID) }) - s.Require().NotEmpty(u.Sub.URL, "user must have a /sub URL") - resp, err := s.API().GetURL(u.Sub.URL) + subURL := u.Sub.SubURL() + s.Require().NotEmpty(subURL, "user must have a /sub URL") + + resp, err := s.API().GetURL(subURL) s.Require().NoError(err) - s.Require().Equal(http.StatusOK, resp.Status, "GET %s", u.Sub.URL) + s.Require().Equal(http.StatusOK, resp.Status, "GET %s", subURL) s.Run("headers", func() { s.Contains(resp.Headers.Get("Content-Type"), "text/yaml") diff --git a/apitest/users/list_test.go b/apitest/users/list_test.go index 7a40632..ee69eaf 100644 --- a/apitest/users/list_test.go +++ b/apitest/users/list_test.go @@ -9,8 +9,9 @@ import ( ) // Corner cases considered for GET /admin/api/users: -// - shape — a created user appears with id/name, a subId + absolute /sub URL, -// and one inbound row per binding (label "<node>-<inbound>", port). +// - shape — a created user appears with id/name, a non-empty subscription +// link list carrying an absolute /sub URL, and one inbound row per +// binding (label "<node>-<inbound>", port). // - traffic — the stats field is present; a brand-new client reports 0/0 // (the users API folds fleet traffic in; fresh clients have none). // - missing_flag — a binding whose client was deleted out-of-band on the panel is @@ -28,10 +29,12 @@ func (s *UserSuite) TestListShape() { s.Equal(u.Name, row.Name) s.Positive(row.ID) - // Subscription coordinates: a non-empty subId and an absolute, token-signed URL. - s.NotEmpty(row.Sub.ID, "row must carry the subId") - s.True(strings.HasPrefix(row.Sub.URL, "http"), "sub URL must be absolute: %q", row.Sub.URL) - s.Contains(row.Sub.URL, "/sub/", "sub URL must hit the /sub route") + // Subscription presentation: a non-empty link list, and the raw /sub URL among the + // links is absolute and hits the /sub route. + s.Require().NotEmpty(row.Sub.Links, "row must carry subscription links") + subURL := row.Sub.SubURL() + s.True(strings.HasPrefix(subURL, "http"), "sub URL must be absolute: %q", subURL) + s.Contains(subURL, "/sub/", "sub URL must hit the /sub route") // Three bindings, labelled "<node>-<inbound>" with the right ports. s.Require().Len(row.Inbounds, 3) diff --git a/apitest/users/suite_test.go b/apitest/users/suite_test.go index 80b003d..9fbfa5f 100644 --- a/apitest/users/suite_test.go +++ b/apitest/users/suite_test.go @@ -84,11 +84,12 @@ func (s *UserSuite) recreateUser(id int64) { // the users API reports) and parses the rendered mihomo YAML into a proxy name->uuid // map — the same ground truth fleet.Sub would give, obtained purely over HTTP. func (s *UserSuite) subProxies(u *api.User) map[string]string { - s.Require().NotEmpty(u.Sub.URL, "user must have a subscription URL") + subURL := u.Sub.SubURL() + s.Require().NotEmpty(subURL, "user must have a subscription URL") - resp, err := s.API().GetURL(u.Sub.URL) + resp, err := s.API().GetURL(subURL) s.Require().NoError(err) - s.Require().Equal(200, resp.Status, "GET %s", u.Sub.URL) + s.Require().Equal(200, resp.Status, "GET %s", subURL) px, err := api.SubProxies(resp.Body) s.Require().NoError(err) diff --git a/cmd/service/main.go b/cmd/service/main.go index 28eae76..10b41de 100644 --- a/cmd/service/main.go +++ b/cmd/service/main.go @@ -60,6 +60,7 @@ import ( nodesService "github.com/postlog/subgen/internal/service/nodes" "github.com/postlog/subgen/internal/service/provisioning" "github.com/postlog/subgen/internal/service/ruleset" + "github.com/postlog/subgen/internal/service/sublinks" ) func main() { @@ -189,6 +190,10 @@ func buildRouter(cfg config.Config, usersRepo *users.Repository, nodesRepo *node nodesSvc := nodesService.New(nodesRepo) + // Subscription-link catalog (raw sub URLs + app deeplinks) for the users list; it + // resolves each user's effective profile title for the deeplink name. + subLinksSvc := sublinks.New(cfg.Secret, cfg.PublicBase, configsRepo, routingRepo) + // The login handler serves both the sign-in action (POST /admin/api/login) and the // login PAGE (GET /admin/login). loginHandler := login.New(sess, cfg.AdminUser, cfg.AdminPassword, cfg.StaticDir) @@ -205,7 +210,7 @@ func buildRouter(cfg config.Config, usersRepo *users.Repository, nodesRepo *node Logout: logout.New(sess), AdminShell: adminShellHandler.New(sess, cfg.StaticDir), - UsersGet: usersGetHandler.New(usersRepo, fleetSvc, cfg.Secret, cfg.PublicBase), + UsersGet: usersGetHandler.New(usersRepo, fleetSvc, subLinksSvc), UserCreate: userCreateHandler.New(prov), UserEdit: userEditHandler.New(prov), UserDelete: userDeleteHandler.New(prov), diff --git a/docs/decisions/0008-subscription-link-catalog.md b/docs/decisions/0008-subscription-link-catalog.md new file mode 100644 index 0000000..a8936c6 --- /dev/null +++ b/docs/decisions/0008-subscription-link-catalog.md @@ -0,0 +1,58 @@ +# 0008 — Каталог ссылок подписки на бэкенде + +- **Статус:** Accepted +- **Дата:** 2026-06-17 +- **PR:** #116 + +## Context + +В списке пользователей колонка «Подписка» была одной кнопкой «Mihomo», копирующей единственный +URL подписки (`sub.url` в ответе `GET /admin/api/users`). Нужно копировать **несколько** +вещей на одного пользователя: сам URL подписки Mihomo и диплинк приложения Clashmi +(`clashmi://install-config?url=<enc>&name=<title>&overwrite=false`), а в будущем — и другие +(новые движки, новые приложения-клиенты). + +Жёсткое требование: **фронт не должен хардкодить, какие ссылки бывают и какие у них тайтлы** — +иначе каждый новый клиент/движок тянет правку SPA. Это противоречит и общему стилю репозитория +(никаких магических строк, признаки — типами/каталогами на бэке). + +## Considered Options + +- **A. Хардкод на фронте.** SPA сам строит clashmi-диплинк из `sub.url` и знает тайтлы. + Минусы: прямое нарушение требования; формат диплинка и список ссылок живут в JS; каждый + новый клиент = правка фронта; дубль логики экранирования. +- **B. `sub.url` + параллельное поле `deeplinks`.** Оставить URL, добавить рядом список + диплинков. Минусы: фронт всё равно знает форму каждого спец-поля; не единообразно; + добавление вида ссылки не каталог-driven — снова правки контракта и фронта. +- **C. Плоский список `sub.links: [{title, value}]`, каталог — в сервисе на бэке (выбрано).** + Ответ несёт готовый упорядоченный список копируемых пар «тайтл → значение»; фронт рендерит + его как есть. Каталог (какие ссылки, их тайтлы, формат диплинка) — в новом + `internal/service/sublinks`. +- Источник `name` для clashmi-диплинка: **(i) profile title эффективного конфига (выбрано)**, + (ii) отдельное env-поле сервиса, (iii) ник пользователя. (i) семантически совпадает с тем, + что подписка уже отдаёт в заголовке `Profile-Title`, и не плодит новой конфигурации. + +## Decision + +Выбран вариант **C**. Новый сервис `internal/service/sublinks` владеет упорядоченным каталогом +`[]linkSpec{ title, kind, build(subURL, profileTitle) }`: для Mihomo `build` — тождество +(сам URL), для Clashmi — формат диплинка с `url.QueryEscape`. `Links(users)` строит на каждого +пользователя его URL (`base + /sub/<kind>/<token>`) и, для диплинков, подставляет `name` = +profile title **эффективного** конфига пользователя (кастомный, иначе базовый). Резолв тайтлов +эффективен: базовый title читается один раз на движок, кастомные — только у тех, у кого они есть. + +Контракт `sub` в `GET /admin/api/users` меняется с `{id, url}` на `{links: [{title, value}]}` +(`id`/`url` убраны — `id` фронтом не использовался, `url` заменён списком). Хендлер `users_get` +делегирует сборку сервису и больше не строит URL сам. + +## Consequences + +- **Добавить движок/приложение = одна строка в `catalog`** — без правок фронта, контракта и + admin-API. Фронт рендерит `sub.links` вербатим (тайтл + кнопка «Копировать»). +- `sub.id`/`sub.url` исчезли из ответа. apitest (чёрный ящик) выбирает «сырой» URL подписки по + схеме (`http…`), а не по тайтлу (`UserSub.SubURL()`), оставаясь агностичным к составу ссылок. +- Резолв тайтлов на страницу — несколько чтений (база + кастомы), не одно-на-пользователя; для + admin-страницы это незаметно. +- Сегодня каталог завязан на mihomo (диплинк Clashmi — это клиент Clash; `sublinks` импортирует + `mihomo.Profile` ради title). Это осознанно: при добавлении xray/sing-box каталог и его + зависимости расширяются явно, без скрытой «магии». diff --git a/internal/entity/sublink.go b/internal/entity/sublink.go new file mode 100644 index 0000000..e36829a --- /dev/null +++ b/internal/entity/sublink.go @@ -0,0 +1,11 @@ +package entity + +// SubLink is one copyable subscription link shown in the admin UI: a display title and +// the literal string to copy. Value is either a raw subscription URL or an app deeplink +// that embeds it (clashmi today). The catalog of which links exist — their titles and +// formats — lives in the sublinks service, so the SPA renders whatever the backend +// declares and hardcodes neither titles nor link formats. +type SubLink struct { + Title string + Value string +} diff --git a/internal/handlers/config_customs/contract.go b/internal/handlers/config_customs/contract.go index 11416f7..e7609e7 100644 --- a/internal/handlers/config_customs/contract.go +++ b/internal/handlers/config_customs/contract.go @@ -7,13 +7,13 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// configLister lists the user ids that have a custom config for an engine. -type configLister interface { +// configsRepo lists the user ids that have a custom config for an engine. +type configsRepo interface { UserConfigUserIDs(ctx context.Context, kind entity.ConfigKind) ([]int64, error) } -// userLister lists every user as id + name (for resolving ids to display names and +// usersRepo lists every user as id + name (for resolving ids to display names and // for the config scope picker) — cheap, no connection hydration. -type userLister interface { +type usersRepo interface { ListNames(ctx context.Context) ([]entity.User, error) } diff --git a/internal/handlers/config_customs/contract_mocks.go b/internal/handlers/config_customs/contract_mocks.go index acb6803..308d6c8 100644 --- a/internal/handlers/config_customs/contract_mocks.go +++ b/internal/handlers/config_customs/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockconfigLister is a mock of configLister interface. -type MockconfigLister struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigListerMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigListerMockRecorder is the mock recorder for MockconfigLister. -type MockconfigListerMockRecorder struct { - mock *MockconfigLister +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigLister creates a new mock instance. -func NewMockconfigLister(ctrl *gomock.Controller) *MockconfigLister { - mock := &MockconfigLister{ctrl: ctrl} - mock.recorder = &MockconfigListerMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigLister) EXPECT() *MockconfigListerMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // UserConfigUserIDs mocks base method. -func (m *MockconfigLister) UserConfigUserIDs(ctx context.Context, kind entity.ConfigKind) ([]int64, error) { +func (m *MockconfigsRepo) UserConfigUserIDs(ctx context.Context, kind entity.ConfigKind) ([]int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserConfigUserIDs", ctx, kind) ret0, _ := ret[0].([]int64) @@ -51,37 +51,37 @@ func (m *MockconfigLister) UserConfigUserIDs(ctx context.Context, kind entity.Co } // UserConfigUserIDs indicates an expected call of UserConfigUserIDs. -func (mr *MockconfigListerMockRecorder) UserConfigUserIDs(ctx, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) UserConfigUserIDs(ctx, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigUserIDs", reflect.TypeOf((*MockconfigLister)(nil).UserConfigUserIDs), ctx, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigUserIDs", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigUserIDs), ctx, kind) } -// MockuserLister is a mock of userLister interface. -type MockuserLister struct { +// MockusersRepo is a mock of usersRepo interface. +type MockusersRepo struct { ctrl *gomock.Controller - recorder *MockuserListerMockRecorder + recorder *MockusersRepoMockRecorder isgomock struct{} } -// MockuserListerMockRecorder is the mock recorder for MockuserLister. -type MockuserListerMockRecorder struct { - mock *MockuserLister +// MockusersRepoMockRecorder is the mock recorder for MockusersRepo. +type MockusersRepoMockRecorder struct { + mock *MockusersRepo } -// NewMockuserLister creates a new mock instance. -func NewMockuserLister(ctrl *gomock.Controller) *MockuserLister { - mock := &MockuserLister{ctrl: ctrl} - mock.recorder = &MockuserListerMockRecorder{mock} +// NewMockusersRepo creates a new mock instance. +func NewMockusersRepo(ctrl *gomock.Controller) *MockusersRepo { + mock := &MockusersRepo{ctrl: ctrl} + mock.recorder = &MockusersRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockuserLister) EXPECT() *MockuserListerMockRecorder { +func (m *MockusersRepo) EXPECT() *MockusersRepoMockRecorder { return m.recorder } // ListNames mocks base method. -func (m *MockuserLister) ListNames(ctx context.Context) ([]entity.User, error) { +func (m *MockusersRepo) ListNames(ctx context.Context) ([]entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListNames", ctx) ret0, _ := ret[0].([]entity.User) @@ -90,7 +90,7 @@ func (m *MockuserLister) ListNames(ctx context.Context) ([]entity.User, error) { } // ListNames indicates an expected call of ListNames. -func (mr *MockuserListerMockRecorder) ListNames(ctx any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) ListNames(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNames", reflect.TypeOf((*MockuserLister)(nil).ListNames), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNames", reflect.TypeOf((*MockusersRepo)(nil).ListNames), ctx) } diff --git a/internal/handlers/config_customs/handler.go b/internal/handlers/config_customs/handler.go index 7ee8e0f..1f20baa 100644 --- a/internal/handlers/config_customs/handler.go +++ b/internal/handlers/config_customs/handler.go @@ -15,12 +15,12 @@ import ( // Handler serves the custom-config owners + the full user list. type Handler struct { - configs configLister - users userLister + configs configsRepo + users usersRepo } // New builds the handler. -func New(configs configLister, users userLister) *Handler { +func New(configs configsRepo, users usersRepo) *Handler { return &Handler{configs: configs, users: users} } diff --git a/internal/handlers/config_customs/handler_test.go b/internal/handlers/config_customs/handler_test.go index c0956ec..91fedd6 100644 --- a/internal/handlers/config_customs/handler_test.go +++ b/internal/handlers/config_customs/handler_test.go @@ -19,18 +19,18 @@ func TestHandler_ConfigCustoms(t *testing.T) { tt := []struct { name string - buildConfigsMock func(m *MockconfigLister) - buildUsersMock func(m *MockuserLister) + buildConfigsMock func(m *MockconfigsRepo) + buildUsersMock func(m *MockusersRepo) result oas.ConfigCustomsRes err error }{ { name: "success", - buildConfigsMock: func(m *MockconfigLister) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return([]int64{2}, nil) }, - buildUsersMock: func(m *MockuserLister) { + buildUsersMock: func(m *MockusersRepo) { m.EXPECT().ListNames(gomock.Any()).Return([]entity.User{ {ID: 1, Name: "alice"}, {ID: 2, Name: "bob"}, @@ -46,17 +46,17 @@ func TestHandler_ConfigCustoms(t *testing.T) { }, { name: "error.list_ids", - buildConfigsMock: func(m *MockconfigLister) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return(nil, internalErr) }, err: internalErr, }, { name: "error.list_names", - buildConfigsMock: func(m *MockconfigLister) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return([]int64{}, nil) }, - buildUsersMock: func(m *MockuserLister) { + buildUsersMock: func(m *MockusersRepo) { m.EXPECT().ListNames(gomock.Any()).Return(nil, internalErr) }, err: internalErr, @@ -70,12 +70,12 @@ func TestHandler_ConfigCustoms(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - configs := NewMockconfigLister(ctrl) + configs := NewMockconfigsRepo(ctrl) if tc.buildConfigsMock != nil { tc.buildConfigsMock(configs) } - users := NewMockuserLister(ctrl) + users := NewMockusersRepo(ctrl) if tc.buildUsersMock != nil { tc.buildUsersMock(users) } diff --git a/internal/handlers/config_get/contract.go b/internal/handlers/config_get/contract.go index 374f74f..bbcdb36 100644 --- a/internal/handlers/config_get/contract.go +++ b/internal/handlers/config_get/contract.go @@ -8,14 +8,14 @@ import ( "github.com/postlog/subgen/internal/mihomo" ) -// configResolver resolves the config scope (base vs a user's custom) to a config id. -type configResolver interface { +// configsRepo resolves the config scope (base vs a user's custom) to a config id. +type configsRepo interface { BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) } -// mihomoReader reads one config's mihomo content (scoped by config id). -type mihomoReader interface { +// routingRepo reads one config's mihomo content (scoped by config id). +type routingRepo interface { Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) diff --git a/internal/handlers/config_get/contract_mocks.go b/internal/handlers/config_get/contract_mocks.go index eddf12d..fdc8048 100644 --- a/internal/handlers/config_get/contract_mocks.go +++ b/internal/handlers/config_get/contract_mocks.go @@ -18,32 +18,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockconfigResolver is a mock of configResolver interface. -type MockconfigResolver struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigResolverMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigResolverMockRecorder is the mock recorder for MockconfigResolver. -type MockconfigResolverMockRecorder struct { - mock *MockconfigResolver +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigResolver creates a new mock instance. -func NewMockconfigResolver(ctrl *gomock.Controller) *MockconfigResolver { - mock := &MockconfigResolver{ctrl: ctrl} - mock.recorder = &MockconfigResolverMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigResolver) EXPECT() *MockconfigResolverMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // BaseConfigID mocks base method. -func (m *MockconfigResolver) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) { +func (m *MockconfigsRepo) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BaseConfigID", ctx, kind) ret0, _ := ret[0].(int64) @@ -53,13 +53,13 @@ func (m *MockconfigResolver) BaseConfigID(ctx context.Context, kind entity.Confi } // BaseConfigID indicates an expected call of BaseConfigID. -func (mr *MockconfigResolverMockRecorder) BaseConfigID(ctx, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) BaseConfigID(ctx, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseConfigID", reflect.TypeOf((*MockconfigResolver)(nil).BaseConfigID), ctx, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).BaseConfigID), ctx, kind) } // UserConfigID mocks base method. -func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { +func (m *MockconfigsRepo) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserConfigID", ctx, userID, kind) ret0, _ := ret[0].(int64) @@ -69,37 +69,37 @@ func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kin } // UserConfigID indicates an expected call of UserConfigID. -func (mr *MockconfigResolverMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigResolver)(nil).UserConfigID), ctx, userID, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigID), ctx, userID, kind) } -// MockmihomoReader is a mock of mihomoReader interface. -type MockmihomoReader struct { +// MockroutingRepo is a mock of routingRepo interface. +type MockroutingRepo struct { ctrl *gomock.Controller - recorder *MockmihomoReaderMockRecorder + recorder *MockroutingRepoMockRecorder isgomock struct{} } -// MockmihomoReaderMockRecorder is the mock recorder for MockmihomoReader. -type MockmihomoReaderMockRecorder struct { - mock *MockmihomoReader +// MockroutingRepoMockRecorder is the mock recorder for MockroutingRepo. +type MockroutingRepoMockRecorder struct { + mock *MockroutingRepo } -// NewMockmihomoReader creates a new mock instance. -func NewMockmihomoReader(ctrl *gomock.Controller) *MockmihomoReader { - mock := &MockmihomoReader{ctrl: ctrl} - mock.recorder = &MockmihomoReaderMockRecorder{mock} +// NewMockroutingRepo creates a new mock instance. +func NewMockroutingRepo(ctrl *gomock.Controller) *MockroutingRepo { + mock := &MockroutingRepo{ctrl: ctrl} + mock.recorder = &MockroutingRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockmihomoReader) EXPECT() *MockmihomoReaderMockRecorder { +func (m *MockroutingRepo) EXPECT() *MockroutingRepoMockRecorder { return m.recorder } // Profile mocks base method. -func (m *MockmihomoReader) Profile(ctx context.Context, configID int64) (mihomo.Profile, error) { +func (m *MockroutingRepo) Profile(ctx context.Context, configID int64) (mihomo.Profile, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Profile", ctx, configID) ret0, _ := ret[0].(mihomo.Profile) @@ -108,13 +108,13 @@ func (m *MockmihomoReader) Profile(ctx context.Context, configID int64) (mihomo. } // Profile indicates an expected call of Profile. -func (mr *MockmihomoReaderMockRecorder) Profile(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Profile(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockmihomoReader)(nil).Profile), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockroutingRepo)(nil).Profile), ctx, configID) } // ProxyGroups mocks base method. -func (m *MockmihomoReader) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) { +func (m *MockroutingRepo) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ProxyGroups", ctx, configID) ret0, _ := ret[0].([]mihomo.ProxyGroup) @@ -123,13 +123,13 @@ func (m *MockmihomoReader) ProxyGroups(ctx context.Context, configID int64) ([]m } // ProxyGroups indicates an expected call of ProxyGroups. -func (mr *MockmihomoReaderMockRecorder) ProxyGroups(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) ProxyGroups(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProxyGroups", reflect.TypeOf((*MockmihomoReader)(nil).ProxyGroups), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProxyGroups", reflect.TypeOf((*MockroutingRepo)(nil).ProxyGroups), ctx, configID) } // RuleProviders mocks base method. -func (m *MockmihomoReader) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) { +func (m *MockroutingRepo) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RuleProviders", ctx, configID) ret0, _ := ret[0].([]mihomo.RuleProvider) @@ -138,13 +138,13 @@ func (m *MockmihomoReader) RuleProviders(ctx context.Context, configID int64) ([ } // RuleProviders indicates an expected call of RuleProviders. -func (mr *MockmihomoReaderMockRecorder) RuleProviders(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) RuleProviders(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleProviders", reflect.TypeOf((*MockmihomoReader)(nil).RuleProviders), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleProviders", reflect.TypeOf((*MockroutingRepo)(nil).RuleProviders), ctx, configID) } // Rules mocks base method. -func (m *MockmihomoReader) Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) { +func (m *MockroutingRepo) Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Rules", ctx, configID) ret0, _ := ret[0].([]mihomo.RoutingRule) @@ -153,13 +153,13 @@ func (m *MockmihomoReader) Rules(ctx context.Context, configID int64) ([]mihomo. } // Rules indicates an expected call of Rules. -func (mr *MockmihomoReaderMockRecorder) Rules(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Rules(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockmihomoReader)(nil).Rules), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockroutingRepo)(nil).Rules), ctx, configID) } // Setting mocks base method. -func (m *MockmihomoReader) Setting(ctx context.Context, configID int64, key string) (string, error) { +func (m *MockroutingRepo) Setting(ctx context.Context, configID int64, key string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Setting", ctx, configID, key) ret0, _ := ret[0].(string) @@ -168,7 +168,7 @@ func (m *MockmihomoReader) Setting(ctx context.Context, configID int64, key stri } // Setting indicates an expected call of Setting. -func (mr *MockmihomoReaderMockRecorder) Setting(ctx, configID, key any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Setting(ctx, configID, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setting", reflect.TypeOf((*MockmihomoReader)(nil).Setting), ctx, configID, key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setting", reflect.TypeOf((*MockroutingRepo)(nil).Setting), ctx, configID, key) } diff --git a/internal/handlers/config_get/handler.go b/internal/handlers/config_get/handler.go index 0331162..4bb6fe3 100644 --- a/internal/handlers/config_get/handler.go +++ b/internal/handlers/config_get/handler.go @@ -15,12 +15,12 @@ import ( // Handler serves a mihomo config (base or a user's custom). type Handler struct { - configs configResolver - routing mihomoReader + configs configsRepo + routing routingRepo } // New builds the handler. -func New(configs configResolver, routing mihomoReader) *Handler { +func New(configs configsRepo, routing routingRepo) *Handler { return &Handler{configs: configs, routing: routing} } diff --git a/internal/handlers/config_get/handler_test.go b/internal/handlers/config_get/handler_test.go index 70c27bf..32983f9 100644 --- a/internal/handlers/config_get/handler_test.go +++ b/internal/handlers/config_get/handler_test.go @@ -22,8 +22,8 @@ func TestHandler_ConfigGet(t *testing.T) { name string params oas.ConfigGetParams - buildConfigsMock func(m *MockconfigResolver) - buildRoutingMock func(m *MockmihomoReader) + buildConfigsMock func(m *MockconfigsRepo) + buildRoutingMock func(m *MockroutingRepo) result oas.ConfigGetRes err error @@ -31,7 +31,7 @@ func TestHandler_ConfigGet(t *testing.T) { { name: "success.base_empty", params: oas.ConfigGetParams{}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(0), false, nil) }, // No config row yet → empty config means empty: profile knobs come back @@ -45,10 +45,10 @@ func TestHandler_ConfigGet(t *testing.T) { { name: "success.base_populated", params: oas.ConfigGetParams{}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(7), true, nil) }, - buildRoutingMock: func(m *MockmihomoReader) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT().Rules(gomock.Any(), int64(7)).Return([]mihomo.RoutingRule{ {Type: mihomo.RuleMatch, Target: &mihomo.PolicyRef{Kind: mihomo.PolicyDirect}}, }, nil) @@ -72,10 +72,10 @@ func TestHandler_ConfigGet(t *testing.T) { // a RULE-SET sub-condition's provider id becomes its array index. name: "success.logical_rule", params: oas.ConfigGetParams{}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(7), true, nil) }, - buildRoutingMock: func(m *MockmihomoReader) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT().Rules(gomock.Any(), int64(7)).Return([]mihomo.RoutingRule{ {Type: mihomo.RuleAnd, Target: &mihomo.PolicyRef{Kind: mihomo.PolicyRejectDrop}, Children: []mihomo.RoutingRule{ {Type: mihomo.RuleNetwork, Value: utils.Ptr("UDP")}, @@ -110,10 +110,10 @@ func TestHandler_ConfigGet(t *testing.T) { // A content read that errors must surface (logged) as a 5xx, not be swallowed. name: "error.read_failed", params: oas.ConfigGetParams{}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(7), true, nil) }, - buildRoutingMock: func(m *MockmihomoReader) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT().Rules(gomock.Any(), int64(7)).Return(nil, internalErr) }, err: internalErr, @@ -121,7 +121,7 @@ func TestHandler_ConfigGet(t *testing.T) { { name: "notfound.user_scope", params: oas.ConfigGetParams{User: oas.NewOptInt64(5)}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigID(gomock.Any(), int64(5), entity.ConfigKindMihomo).Return(int64(0), false, nil) }, result: &oas.ConfigGetNotFound{}, @@ -129,7 +129,7 @@ func TestHandler_ConfigGet(t *testing.T) { { name: "error.internal", params: oas.ConfigGetParams{User: oas.NewOptInt64(5)}, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigID(gomock.Any(), int64(5), entity.ConfigKindMihomo).Return(int64(0), false, internalErr) }, err: internalErr, @@ -143,12 +143,12 @@ func TestHandler_ConfigGet(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - configs := NewMockconfigResolver(ctrl) + configs := NewMockconfigsRepo(ctrl) if tc.buildConfigsMock != nil { tc.buildConfigsMock(configs) } - routing := NewMockmihomoReader(ctrl) + routing := NewMockroutingRepo(ctrl) if tc.buildRoutingMock != nil { tc.buildRoutingMock(routing) } diff --git a/internal/handlers/config_save/contract.go b/internal/handlers/config_save/contract.go index f5e8406..51f98db 100644 --- a/internal/handlers/config_save/contract.go +++ b/internal/handlers/config_save/contract.go @@ -8,16 +8,16 @@ import ( "github.com/postlog/subgen/internal/mihomo" ) -// configResolver resolves the save scope to a config id: the base (created on first +// configsRepo resolves the save scope to a config id: the base (created on first // save) or a user's existing custom config. -type configResolver interface { +type configsRepo interface { EnsureBaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, error) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) } -// mihomoSaver persists one config's mihomo content (rules + proxy-groups + providers +// routingRepo persists one config's mihomo content (rules + proxy-groups + providers // + base YAML + profile knobs) atomically, scoped by config id. It takes a ConfigDraft // — group/provider references are carried as array indices, resolved to ids on insert. -type mihomoSaver interface { +type routingRepo interface { SaveMihomoConfig(ctx context.Context, configID int64, draft mihomo.ConfigDraft) error } diff --git a/internal/handlers/config_save/contract_mocks.go b/internal/handlers/config_save/contract_mocks.go index 6d57ec4..66cb314 100644 --- a/internal/handlers/config_save/contract_mocks.go +++ b/internal/handlers/config_save/contract_mocks.go @@ -18,32 +18,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockconfigResolver is a mock of configResolver interface. -type MockconfigResolver struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigResolverMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigResolverMockRecorder is the mock recorder for MockconfigResolver. -type MockconfigResolverMockRecorder struct { - mock *MockconfigResolver +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigResolver creates a new mock instance. -func NewMockconfigResolver(ctrl *gomock.Controller) *MockconfigResolver { - mock := &MockconfigResolver{ctrl: ctrl} - mock.recorder = &MockconfigResolverMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigResolver) EXPECT() *MockconfigResolverMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // EnsureBaseConfigID mocks base method. -func (m *MockconfigResolver) EnsureBaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, error) { +func (m *MockconfigsRepo) EnsureBaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EnsureBaseConfigID", ctx, kind) ret0, _ := ret[0].(int64) @@ -52,13 +52,13 @@ func (m *MockconfigResolver) EnsureBaseConfigID(ctx context.Context, kind entity } // EnsureBaseConfigID indicates an expected call of EnsureBaseConfigID. -func (mr *MockconfigResolverMockRecorder) EnsureBaseConfigID(ctx, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) EnsureBaseConfigID(ctx, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBaseConfigID", reflect.TypeOf((*MockconfigResolver)(nil).EnsureBaseConfigID), ctx, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBaseConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).EnsureBaseConfigID), ctx, kind) } // UserConfigID mocks base method. -func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { +func (m *MockconfigsRepo) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserConfigID", ctx, userID, kind) ret0, _ := ret[0].(int64) @@ -68,37 +68,37 @@ func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kin } // UserConfigID indicates an expected call of UserConfigID. -func (mr *MockconfigResolverMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigResolver)(nil).UserConfigID), ctx, userID, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigID), ctx, userID, kind) } -// MockmihomoSaver is a mock of mihomoSaver interface. -type MockmihomoSaver struct { +// MockroutingRepo is a mock of routingRepo interface. +type MockroutingRepo struct { ctrl *gomock.Controller - recorder *MockmihomoSaverMockRecorder + recorder *MockroutingRepoMockRecorder isgomock struct{} } -// MockmihomoSaverMockRecorder is the mock recorder for MockmihomoSaver. -type MockmihomoSaverMockRecorder struct { - mock *MockmihomoSaver +// MockroutingRepoMockRecorder is the mock recorder for MockroutingRepo. +type MockroutingRepoMockRecorder struct { + mock *MockroutingRepo } -// NewMockmihomoSaver creates a new mock instance. -func NewMockmihomoSaver(ctrl *gomock.Controller) *MockmihomoSaver { - mock := &MockmihomoSaver{ctrl: ctrl} - mock.recorder = &MockmihomoSaverMockRecorder{mock} +// NewMockroutingRepo creates a new mock instance. +func NewMockroutingRepo(ctrl *gomock.Controller) *MockroutingRepo { + mock := &MockroutingRepo{ctrl: ctrl} + mock.recorder = &MockroutingRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockmihomoSaver) EXPECT() *MockmihomoSaverMockRecorder { +func (m *MockroutingRepo) EXPECT() *MockroutingRepoMockRecorder { return m.recorder } // SaveMihomoConfig mocks base method. -func (m *MockmihomoSaver) SaveMihomoConfig(ctx context.Context, configID int64, draft mihomo.ConfigDraft) error { +func (m *MockroutingRepo) SaveMihomoConfig(ctx context.Context, configID int64, draft mihomo.ConfigDraft) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveMihomoConfig", ctx, configID, draft) ret0, _ := ret[0].(error) @@ -106,7 +106,7 @@ func (m *MockmihomoSaver) SaveMihomoConfig(ctx context.Context, configID int64, } // SaveMihomoConfig indicates an expected call of SaveMihomoConfig. -func (mr *MockmihomoSaverMockRecorder) SaveMihomoConfig(ctx, configID, draft any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) SaveMihomoConfig(ctx, configID, draft any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveMihomoConfig", reflect.TypeOf((*MockmihomoSaver)(nil).SaveMihomoConfig), ctx, configID, draft) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveMihomoConfig", reflect.TypeOf((*MockroutingRepo)(nil).SaveMihomoConfig), ctx, configID, draft) } diff --git a/internal/handlers/config_save/handler.go b/internal/handlers/config_save/handler.go index 00803a7..131b3f6 100644 --- a/internal/handlers/config_save/handler.go +++ b/internal/handlers/config_save/handler.go @@ -59,12 +59,12 @@ const ( // Handler saves a mihomo config (base or a user's custom). type Handler struct { - configs configResolver - routing mihomoSaver + configs configsRepo + routing routingRepo } // New builds the handler. -func New(configs configResolver, routing mihomoSaver) *Handler { +func New(configs configsRepo, routing routingRepo) *Handler { return &Handler{configs: configs, routing: routing} } diff --git a/internal/handlers/config_save/handler_test.go b/internal/handlers/config_save/handler_test.go index ed017ac..782100f 100644 --- a/internal/handlers/config_save/handler_test.go +++ b/internal/handlers/config_save/handler_test.go @@ -50,8 +50,8 @@ func TestHandler_ConfigSave(t *testing.T) { name string req *oas.ConfigSaveReq - buildConfigsMock func(m *MockconfigResolver) - buildRoutingMock func(m *MockmihomoSaver) + buildConfigsMock func(m *MockconfigsRepo) + buildRoutingMock func(m *MockroutingRepo) result oas.ConfigSaveRes err error @@ -59,10 +59,10 @@ func TestHandler_ConfigSave(t *testing.T) { { name: "success.base", req: validReq(0), - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().EnsureBaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(3), nil) }, - buildRoutingMock: func(m *MockmihomoSaver) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT(). SaveMihomoConfig(gomock.Any(), int64(3), wantDraft). Return(nil) @@ -87,10 +87,10 @@ func TestHandler_ConfigSave(t *testing.T) { Filename: "my.yaml", ProfileUpdateInterval: 6, }, - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().EnsureBaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(3), nil) }, - buildRoutingMock: func(m *MockmihomoSaver) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT().SaveMihomoConfig(gomock.Any(), int64(3), mihomo.ConfigDraft{ Rules: []mihomo.RuleDraft{ {Type: mihomo.RuleAnd, Target: &mihomo.RefDraft{Kind: mihomo.PolicyRejectDrop}, Children: []mihomo.RuleDraft{ @@ -162,7 +162,7 @@ func TestHandler_ConfigSave(t *testing.T) { { name: "error.user_config_not_found", req: validReq(5), - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().UserConfigID(gomock.Any(), int64(5), entity.ConfigKindMihomo).Return(int64(0), false, nil) }, result: &oas.ConfigSaveBadRequest{ErrMessage: MsgUserConfigMissing}, @@ -170,10 +170,10 @@ func TestHandler_ConfigSave(t *testing.T) { { name: "error.save_provider_taken", req: validReq(0), - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().EnsureBaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(3), nil) }, - buildRoutingMock: func(m *MockmihomoSaver) { + buildRoutingMock: func(m *MockroutingRepo) { m.EXPECT(). SaveMihomoConfig(gomock.Any(), int64(3), wantDraft). Return(entity.ErrRuleProviderNameTaken) @@ -183,7 +183,7 @@ func TestHandler_ConfigSave(t *testing.T) { { name: "error.internal_resolve", req: validReq(0), - buildConfigsMock: func(m *MockconfigResolver) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT().EnsureBaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(0), internalErr) }, err: internalErr, @@ -197,12 +197,12 @@ func TestHandler_ConfigSave(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - configs := NewMockconfigResolver(ctrl) + configs := NewMockconfigsRepo(ctrl) if tc.buildConfigsMock != nil { tc.buildConfigsMock(configs) } - routing := NewMockmihomoSaver(ctrl) + routing := NewMockroutingRepo(ctrl) if tc.buildRoutingMock != nil { tc.buildRoutingMock(routing) } diff --git a/internal/handlers/custom_create/contract.go b/internal/handlers/custom_create/contract.go index e015756..f7fd804 100644 --- a/internal/handlers/custom_create/contract.go +++ b/internal/handlers/custom_create/contract.go @@ -7,7 +7,7 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// configCreator creates a user's custom config as a snapshot of the engine's base. -type configCreator interface { +// configsRepo creates a user's custom config as a snapshot of the engine's base. +type configsRepo interface { CreateUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, error) } diff --git a/internal/handlers/custom_create/contract_mocks.go b/internal/handlers/custom_create/contract_mocks.go index db46f7d..edc57bb 100644 --- a/internal/handlers/custom_create/contract_mocks.go +++ b/internal/handlers/custom_create/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockconfigCreator is a mock of configCreator interface. -type MockconfigCreator struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigCreatorMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigCreatorMockRecorder is the mock recorder for MockconfigCreator. -type MockconfigCreatorMockRecorder struct { - mock *MockconfigCreator +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigCreator creates a new mock instance. -func NewMockconfigCreator(ctrl *gomock.Controller) *MockconfigCreator { - mock := &MockconfigCreator{ctrl: ctrl} - mock.recorder = &MockconfigCreatorMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigCreator) EXPECT() *MockconfigCreatorMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // CreateUserConfig mocks base method. -func (m *MockconfigCreator) CreateUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, error) { +func (m *MockconfigsRepo) CreateUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateUserConfig", ctx, userID, kind) ret0, _ := ret[0].(int64) @@ -51,7 +51,7 @@ func (m *MockconfigCreator) CreateUserConfig(ctx context.Context, userID int64, } // CreateUserConfig indicates an expected call of CreateUserConfig. -func (mr *MockconfigCreatorMockRecorder) CreateUserConfig(ctx, userID, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) CreateUserConfig(ctx, userID, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserConfig", reflect.TypeOf((*MockconfigCreator)(nil).CreateUserConfig), ctx, userID, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserConfig", reflect.TypeOf((*MockconfigsRepo)(nil).CreateUserConfig), ctx, userID, kind) } diff --git a/internal/handlers/custom_create/handler.go b/internal/handlers/custom_create/handler.go index 1ba7950..ce57705 100644 --- a/internal/handlers/custom_create/handler.go +++ b/internal/handlers/custom_create/handler.go @@ -16,11 +16,11 @@ const msgConfigExists = "У пользователя уже есть касто // Handler clones the base config into a new per-user custom config. type Handler struct { - configs configCreator + configs configsRepo } // New builds the handler. -func New(configs configCreator) *Handler { return &Handler{configs: configs} } +func New(configs configsRepo) *Handler { return &Handler{configs: configs} } // CustomCreate implements oas.Handler: an already-existing custom config is a 400; any // unexpected (store) failure is a 500. diff --git a/internal/handlers/custom_create/handler_test.go b/internal/handlers/custom_create/handler_test.go index ff293f4..269d603 100644 --- a/internal/handlers/custom_create/handler_test.go +++ b/internal/handlers/custom_create/handler_test.go @@ -20,7 +20,7 @@ func TestHandler_CustomCreate(t *testing.T) { name string req *oas.CustomCreateReq - buildConfigsMock func(m *MockconfigCreator) + buildConfigsMock func(m *MockconfigsRepo) result oas.CustomCreateRes err error @@ -28,7 +28,7 @@ func TestHandler_CustomCreate(t *testing.T) { { name: "success", req: &oas.CustomCreateReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigCreator) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). CreateUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(int64(42), nil) @@ -38,7 +38,7 @@ func TestHandler_CustomCreate(t *testing.T) { { name: "error.exists", req: &oas.CustomCreateReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigCreator) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). CreateUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(int64(0), entity.ErrUserConfigExists) @@ -48,7 +48,7 @@ func TestHandler_CustomCreate(t *testing.T) { { name: "error.internal", req: &oas.CustomCreateReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigCreator) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). CreateUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(int64(0), internalErr) @@ -64,7 +64,7 @@ func TestHandler_CustomCreate(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - configs := NewMockconfigCreator(ctrl) + configs := NewMockconfigsRepo(ctrl) if tc.buildConfigsMock != nil { tc.buildConfigsMock(configs) } diff --git a/internal/handlers/custom_delete/contract.go b/internal/handlers/custom_delete/contract.go index 5e22aea..458d2df 100644 --- a/internal/handlers/custom_delete/contract.go +++ b/internal/handlers/custom_delete/contract.go @@ -7,7 +7,7 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// configDeleter removes a user's custom config (its content cascades away). -type configDeleter interface { +// configsRepo removes a user's custom config (its content cascades away). +type configsRepo interface { DeleteUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) error } diff --git a/internal/handlers/custom_delete/contract_mocks.go b/internal/handlers/custom_delete/contract_mocks.go index 7afc7a8..d2beaf7 100644 --- a/internal/handlers/custom_delete/contract_mocks.go +++ b/internal/handlers/custom_delete/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockconfigDeleter is a mock of configDeleter interface. -type MockconfigDeleter struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigDeleterMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigDeleterMockRecorder is the mock recorder for MockconfigDeleter. -type MockconfigDeleterMockRecorder struct { - mock *MockconfigDeleter +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigDeleter creates a new mock instance. -func NewMockconfigDeleter(ctrl *gomock.Controller) *MockconfigDeleter { - mock := &MockconfigDeleter{ctrl: ctrl} - mock.recorder = &MockconfigDeleterMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigDeleter) EXPECT() *MockconfigDeleterMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // DeleteUserConfig mocks base method. -func (m *MockconfigDeleter) DeleteUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) error { +func (m *MockconfigsRepo) DeleteUserConfig(ctx context.Context, userID int64, kind entity.ConfigKind) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteUserConfig", ctx, userID, kind) ret0, _ := ret[0].(error) @@ -50,7 +50,7 @@ func (m *MockconfigDeleter) DeleteUserConfig(ctx context.Context, userID int64, } // DeleteUserConfig indicates an expected call of DeleteUserConfig. -func (mr *MockconfigDeleterMockRecorder) DeleteUserConfig(ctx, userID, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) DeleteUserConfig(ctx, userID, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserConfig", reflect.TypeOf((*MockconfigDeleter)(nil).DeleteUserConfig), ctx, userID, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserConfig", reflect.TypeOf((*MockconfigsRepo)(nil).DeleteUserConfig), ctx, userID, kind) } diff --git a/internal/handlers/custom_delete/handler.go b/internal/handlers/custom_delete/handler.go index 4a7d528..0f73fc2 100644 --- a/internal/handlers/custom_delete/handler.go +++ b/internal/handlers/custom_delete/handler.go @@ -16,11 +16,11 @@ const msgConfigMissing = "У пользователя нет кастомног // Handler drops a user's custom config. type Handler struct { - configs configDeleter + configs configsRepo } // New builds the handler. -func New(configs configDeleter) *Handler { return &Handler{configs: configs} } +func New(configs configsRepo) *Handler { return &Handler{configs: configs} } // CustomDelete implements oas.Handler: a user with no custom config is a 400; any // unexpected (store) failure is a 500. diff --git a/internal/handlers/custom_delete/handler_test.go b/internal/handlers/custom_delete/handler_test.go index 94118dc..8f8f355 100644 --- a/internal/handlers/custom_delete/handler_test.go +++ b/internal/handlers/custom_delete/handler_test.go @@ -20,7 +20,7 @@ func TestHandler_CustomDelete(t *testing.T) { name string req *oas.CustomDeleteReq - buildConfigsMock func(m *MockconfigDeleter) + buildConfigsMock func(m *MockconfigsRepo) result oas.CustomDeleteRes err error @@ -28,7 +28,7 @@ func TestHandler_CustomDelete(t *testing.T) { { name: "success", req: &oas.CustomDeleteReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigDeleter) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). DeleteUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(nil) @@ -38,7 +38,7 @@ func TestHandler_CustomDelete(t *testing.T) { { name: "error.missing", req: &oas.CustomDeleteReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigDeleter) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). DeleteUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(entity.ErrUserConfigNotFound) @@ -48,7 +48,7 @@ func TestHandler_CustomDelete(t *testing.T) { { name: "error.internal", req: &oas.CustomDeleteReq{UserId: 7}, - buildConfigsMock: func(m *MockconfigDeleter) { + buildConfigsMock: func(m *MockconfigsRepo) { m.EXPECT(). DeleteUserConfig(gomock.Any(), int64(7), entity.ConfigKindMihomo). Return(internalErr) @@ -64,7 +64,7 @@ func TestHandler_CustomDelete(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - configs := NewMockconfigDeleter(ctrl) + configs := NewMockconfigsRepo(ctrl) if tc.buildConfigsMock != nil { tc.buildConfigsMock(configs) } diff --git a/internal/handlers/node_delete/contract.go b/internal/handlers/node_delete/contract.go index 224c5a5..6bb69d0 100644 --- a/internal/handlers/node_delete/contract.go +++ b/internal/handlers/node_delete/contract.go @@ -3,9 +3,9 @@ package node_delete import "context" -// nodeDeleter is the nodes service: it deletes a node (the nodes service satisfies it). A +// nodesService is the nodes service: it deletes a node (the nodes service satisfies it). A // node whose inbound is still referenced is refused by the database FK, returned as // entity.ErrInboundReferenced. -type nodeDeleter interface { +type nodesService interface { Delete(ctx context.Context, id int64) error } diff --git a/internal/handlers/node_delete/contract_mocks.go b/internal/handlers/node_delete/contract_mocks.go index 190dbd6..9626b97 100644 --- a/internal/handlers/node_delete/contract_mocks.go +++ b/internal/handlers/node_delete/contract_mocks.go @@ -16,32 +16,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MocknodeDeleter is a mock of nodeDeleter interface. -type MocknodeDeleter struct { +// MocknodesService is a mock of nodesService interface. +type MocknodesService struct { ctrl *gomock.Controller - recorder *MocknodeDeleterMockRecorder + recorder *MocknodesServiceMockRecorder isgomock struct{} } -// MocknodeDeleterMockRecorder is the mock recorder for MocknodeDeleter. -type MocknodeDeleterMockRecorder struct { - mock *MocknodeDeleter +// MocknodesServiceMockRecorder is the mock recorder for MocknodesService. +type MocknodesServiceMockRecorder struct { + mock *MocknodesService } -// NewMocknodeDeleter creates a new mock instance. -func NewMocknodeDeleter(ctrl *gomock.Controller) *MocknodeDeleter { - mock := &MocknodeDeleter{ctrl: ctrl} - mock.recorder = &MocknodeDeleterMockRecorder{mock} +// NewMocknodesService creates a new mock instance. +func NewMocknodesService(ctrl *gomock.Controller) *MocknodesService { + mock := &MocknodesService{ctrl: ctrl} + mock.recorder = &MocknodesServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeDeleter) EXPECT() *MocknodeDeleterMockRecorder { +func (m *MocknodesService) EXPECT() *MocknodesServiceMockRecorder { return m.recorder } // Delete mocks base method. -func (m *MocknodeDeleter) Delete(ctx context.Context, id int64) error { +func (m *MocknodesService) Delete(ctx context.Context, id int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ctx, id) ret0, _ := ret[0].(error) @@ -49,7 +49,7 @@ func (m *MocknodeDeleter) Delete(ctx context.Context, id int64) error { } // Delete indicates an expected call of Delete. -func (mr *MocknodeDeleterMockRecorder) Delete(ctx, id any) *gomock.Call { +func (mr *MocknodesServiceMockRecorder) Delete(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MocknodeDeleter)(nil).Delete), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MocknodesService)(nil).Delete), ctx, id) } diff --git a/internal/handlers/node_delete/handler.go b/internal/handlers/node_delete/handler.go index 6bd8a63..9b8097e 100644 --- a/internal/handlers/node_delete/handler.go +++ b/internal/handlers/node_delete/handler.go @@ -22,11 +22,11 @@ const ( // Handler deletes a node via the nodes service. type Handler struct { - svc nodeDeleter + svc nodesService } // New builds the handler. -func New(svc nodeDeleter) *Handler { return &Handler{svc: svc} } +func New(svc nodesService) *Handler { return &Handler{svc: svc} } // NodeDelete implements oas.Handler: a still-referenced inbound is a 400, any unexpected // (store) failure is a 500. diff --git a/internal/handlers/node_delete/handler_test.go b/internal/handlers/node_delete/handler_test.go index 9978fea..2ab56b4 100644 --- a/internal/handlers/node_delete/handler_test.go +++ b/internal/handlers/node_delete/handler_test.go @@ -35,7 +35,7 @@ func TestHandler_NodeDelete(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMocknodeDeleter(ctrl) + svc := NewMocknodesService(ctrl) svc.EXPECT().Delete(gomock.Any(), int64(7)).Return(tc.deleteErr) res, err := New(svc).NodeDelete(context.Background(), &oas.NodeDeleteReq{ID: 7}) diff --git a/internal/handlers/node_save/contract.go b/internal/handlers/node_save/contract.go index 14e7264..122eb81 100644 --- a/internal/handlers/node_save/contract.go +++ b/internal/handlers/node_save/contract.go @@ -7,8 +7,8 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// nodeSaver is the nodes service: it validates and persists a node (the nodes service +// nodesService is the nodes service: it validates and persists a node (the nodes service // satisfies it). -type nodeSaver interface { +type nodesService interface { Save(ctx context.Context, n entity.Node) (int64, error) } diff --git a/internal/handlers/node_save/contract_mocks.go b/internal/handlers/node_save/contract_mocks.go index d574751..8f7954a 100644 --- a/internal/handlers/node_save/contract_mocks.go +++ b/internal/handlers/node_save/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MocknodeSaver is a mock of nodeSaver interface. -type MocknodeSaver struct { +// MocknodesService is a mock of nodesService interface. +type MocknodesService struct { ctrl *gomock.Controller - recorder *MocknodeSaverMockRecorder + recorder *MocknodesServiceMockRecorder isgomock struct{} } -// MocknodeSaverMockRecorder is the mock recorder for MocknodeSaver. -type MocknodeSaverMockRecorder struct { - mock *MocknodeSaver +// MocknodesServiceMockRecorder is the mock recorder for MocknodesService. +type MocknodesServiceMockRecorder struct { + mock *MocknodesService } -// NewMocknodeSaver creates a new mock instance. -func NewMocknodeSaver(ctrl *gomock.Controller) *MocknodeSaver { - mock := &MocknodeSaver{ctrl: ctrl} - mock.recorder = &MocknodeSaverMockRecorder{mock} +// NewMocknodesService creates a new mock instance. +func NewMocknodesService(ctrl *gomock.Controller) *MocknodesService { + mock := &MocknodesService{ctrl: ctrl} + mock.recorder = &MocknodesServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeSaver) EXPECT() *MocknodeSaverMockRecorder { +func (m *MocknodesService) EXPECT() *MocknodesServiceMockRecorder { return m.recorder } // Save mocks base method. -func (m *MocknodeSaver) Save(ctx context.Context, n entity.Node) (int64, error) { +func (m *MocknodesService) Save(ctx context.Context, n entity.Node) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Save", ctx, n) ret0, _ := ret[0].(int64) @@ -51,7 +51,7 @@ func (m *MocknodeSaver) Save(ctx context.Context, n entity.Node) (int64, error) } // Save indicates an expected call of Save. -func (mr *MocknodeSaverMockRecorder) Save(ctx, n any) *gomock.Call { +func (mr *MocknodesServiceMockRecorder) Save(ctx, n any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MocknodeSaver)(nil).Save), ctx, n) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MocknodesService)(nil).Save), ctx, n) } diff --git a/internal/handlers/node_save/handler.go b/internal/handlers/node_save/handler.go index 947bc8f..5beffd2 100644 --- a/internal/handlers/node_save/handler.go +++ b/internal/handlers/node_save/handler.go @@ -34,11 +34,11 @@ const ( // Handler creates or updates a node from the node form. type Handler struct { - svc nodeSaver + svc nodesService } // New builds the handler. -func New(svc nodeSaver) *Handler { return &Handler{svc: svc} } +func New(svc nodesService) *Handler { return &Handler{svc: svc} } // NodeSave implements oas.Handler: a name/inbound clash is a 409, invalid input or a // still-referenced dropped inbound is a 400, any unexpected (store) failure is a 500. diff --git a/internal/handlers/node_save/handler_test.go b/internal/handlers/node_save/handler_test.go index 7896e52..d803a15 100644 --- a/internal/handlers/node_save/handler_test.go +++ b/internal/handlers/node_save/handler_test.go @@ -61,7 +61,7 @@ func TestHandler_NodeSave(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMocknodeSaver(ctrl) + svc := NewMocknodesService(ctrl) svc.EXPECT().Save(gomock.Any(), wantNode()).Return(int64(1), tc.saveErr) res, err := New(svc).NodeSave(context.Background(), nodeReq()) diff --git a/internal/handlers/nodes_get/contract.go b/internal/handlers/nodes_get/contract.go index 150562d..49d9de5 100644 --- a/internal/handlers/nodes_get/contract.go +++ b/internal/handlers/nodes_get/contract.go @@ -7,6 +7,6 @@ import ( "github.com/postlog/subgen/internal/entity" ) -type nodeLister interface { +type nodesRepo interface { List(ctx context.Context) ([]entity.Node, error) } diff --git a/internal/handlers/nodes_get/contract_mocks.go b/internal/handlers/nodes_get/contract_mocks.go index 089157d..bab4b91 100644 --- a/internal/handlers/nodes_get/contract_mocks.go +++ b/internal/handlers/nodes_get/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MocknodeLister is a mock of nodeLister interface. -type MocknodeLister struct { +// MocknodesRepo is a mock of nodesRepo interface. +type MocknodesRepo struct { ctrl *gomock.Controller - recorder *MocknodeListerMockRecorder + recorder *MocknodesRepoMockRecorder isgomock struct{} } -// MocknodeListerMockRecorder is the mock recorder for MocknodeLister. -type MocknodeListerMockRecorder struct { - mock *MocknodeLister +// MocknodesRepoMockRecorder is the mock recorder for MocknodesRepo. +type MocknodesRepoMockRecorder struct { + mock *MocknodesRepo } -// NewMocknodeLister creates a new mock instance. -func NewMocknodeLister(ctrl *gomock.Controller) *MocknodeLister { - mock := &MocknodeLister{ctrl: ctrl} - mock.recorder = &MocknodeListerMockRecorder{mock} +// NewMocknodesRepo creates a new mock instance. +func NewMocknodesRepo(ctrl *gomock.Controller) *MocknodesRepo { + mock := &MocknodesRepo{ctrl: ctrl} + mock.recorder = &MocknodesRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeLister) EXPECT() *MocknodeListerMockRecorder { +func (m *MocknodesRepo) EXPECT() *MocknodesRepoMockRecorder { return m.recorder } // List mocks base method. -func (m *MocknodeLister) List(ctx context.Context) ([]entity.Node, error) { +func (m *MocknodesRepo) List(ctx context.Context) ([]entity.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ctx) ret0, _ := ret[0].([]entity.Node) @@ -51,7 +51,7 @@ func (m *MocknodeLister) List(ctx context.Context) ([]entity.Node, error) { } // List indicates an expected call of List. -func (mr *MocknodeListerMockRecorder) List(ctx any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) List(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodeLister)(nil).List), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodesRepo)(nil).List), ctx) } diff --git a/internal/handlers/nodes_get/handler.go b/internal/handlers/nodes_get/handler.go index 82181b1..56d01c2 100644 --- a/internal/handlers/nodes_get/handler.go +++ b/internal/handlers/nodes_get/handler.go @@ -12,11 +12,11 @@ import ( // Handler serves the node registry. type Handler struct { - nodes nodeLister + nodes nodesRepo } // New builds the handler. -func New(nodes nodeLister) *Handler { return &Handler{nodes: nodes} } +func New(nodes nodesRepo) *Handler { return &Handler{nodes: nodes} } // NodesGet implements oas.Handler: it lists the fleet nodes with their inbounds. func (h *Handler) NodesGet(ctx context.Context) (oas.NodesGetRes, error) { diff --git a/internal/handlers/nodes_get/handler_test.go b/internal/handlers/nodes_get/handler_test.go index b9ad627..12a1e47 100644 --- a/internal/handlers/nodes_get/handler_test.go +++ b/internal/handlers/nodes_get/handler_test.go @@ -19,14 +19,14 @@ func TestHandler_NodesGet(t *testing.T) { tt := []struct { name string - buildNodesMock func(m *MocknodeLister) + buildNodesMock func(m *MocknodesRepo) result oas.NodesGetRes err error }{ { name: "success", - buildNodesMock: func(m *MocknodeLister) { + buildNodesMock: func(m *MocknodesRepo) { m.EXPECT().List(gomock.Any()).Return([]entity.Node{{ ID: 1, Name: "RU1", @@ -53,14 +53,14 @@ func TestHandler_NodesGet(t *testing.T) { }, { name: "empty", - buildNodesMock: func(m *MocknodeLister) { + buildNodesMock: func(m *MocknodesRepo) { m.EXPECT().List(gomock.Any()).Return([]entity.Node{}, nil) }, result: &oas.NodesGetOK{Nodes: []oas.NodesGetOKNodesItem{}}, }, { name: "error.list", - buildNodesMock: func(m *MocknodeLister) { + buildNodesMock: func(m *MocknodesRepo) { m.EXPECT().List(gomock.Any()).Return(nil, internalErr) }, err: internalErr, @@ -74,7 +74,7 @@ func TestHandler_NodesGet(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - nodes := NewMocknodeLister(ctrl) + nodes := NewMocknodesRepo(ctrl) if tc.buildNodesMock != nil { tc.buildNodesMock(nodes) } diff --git a/internal/handlers/provider_check/contract.go b/internal/handlers/provider_check/contract.go index b544ab1..383eacc 100644 --- a/internal/handlers/provider_check/contract.go +++ b/internal/handlers/provider_check/contract.go @@ -7,7 +7,7 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// providerChecker probes a rule-provider URL (reachable / present / right format). -type providerChecker interface { +// rulesetChecker probes a rule-provider URL (reachable / present / right format). +type rulesetChecker interface { Check(ctx context.Context, url, format string) entity.RulesetCheckResult } diff --git a/internal/handlers/provider_check/contract_mocks.go b/internal/handlers/provider_check/contract_mocks.go index 6585283..f59294c 100644 --- a/internal/handlers/provider_check/contract_mocks.go +++ b/internal/handlers/provider_check/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockproviderChecker is a mock of providerChecker interface. -type MockproviderChecker struct { +// MockrulesetChecker is a mock of rulesetChecker interface. +type MockrulesetChecker struct { ctrl *gomock.Controller - recorder *MockproviderCheckerMockRecorder + recorder *MockrulesetCheckerMockRecorder isgomock struct{} } -// MockproviderCheckerMockRecorder is the mock recorder for MockproviderChecker. -type MockproviderCheckerMockRecorder struct { - mock *MockproviderChecker +// MockrulesetCheckerMockRecorder is the mock recorder for MockrulesetChecker. +type MockrulesetCheckerMockRecorder struct { + mock *MockrulesetChecker } -// NewMockproviderChecker creates a new mock instance. -func NewMockproviderChecker(ctrl *gomock.Controller) *MockproviderChecker { - mock := &MockproviderChecker{ctrl: ctrl} - mock.recorder = &MockproviderCheckerMockRecorder{mock} +// NewMockrulesetChecker creates a new mock instance. +func NewMockrulesetChecker(ctrl *gomock.Controller) *MockrulesetChecker { + mock := &MockrulesetChecker{ctrl: ctrl} + mock.recorder = &MockrulesetCheckerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockproviderChecker) EXPECT() *MockproviderCheckerMockRecorder { +func (m *MockrulesetChecker) EXPECT() *MockrulesetCheckerMockRecorder { return m.recorder } // Check mocks base method. -func (m *MockproviderChecker) Check(ctx context.Context, url, format string) entity.RulesetCheckResult { +func (m *MockrulesetChecker) Check(ctx context.Context, url, format string) entity.RulesetCheckResult { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Check", ctx, url, format) ret0, _ := ret[0].(entity.RulesetCheckResult) @@ -50,7 +50,7 @@ func (m *MockproviderChecker) Check(ctx context.Context, url, format string) ent } // Check indicates an expected call of Check. -func (mr *MockproviderCheckerMockRecorder) Check(ctx, url, format any) *gomock.Call { +func (mr *MockrulesetCheckerMockRecorder) Check(ctx, url, format any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockproviderChecker)(nil).Check), ctx, url, format) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockrulesetChecker)(nil).Check), ctx, url, format) } diff --git a/internal/handlers/provider_check/handler.go b/internal/handlers/provider_check/handler.go index b2d7a57..6817b1d 100644 --- a/internal/handlers/provider_check/handler.go +++ b/internal/handlers/provider_check/handler.go @@ -23,11 +23,11 @@ const ( // Handler probes a rule-provider URL via the checker service. type Handler struct { - checker providerChecker + checker rulesetChecker } // New builds the handler. -func New(checker providerChecker) *Handler { return &Handler{checker: checker} } +func New(checker rulesetChecker) *Handler { return &Handler{checker: checker} } // ProviderCheck implements oas.Handler: a reachable, right-format file is a 200 with a // message; any other outcome is a 400. The handler does not validate the URL — a blank or diff --git a/internal/handlers/provider_check/handler_test.go b/internal/handlers/provider_check/handler_test.go index 0779ba4..e5108d8 100644 --- a/internal/handlers/provider_check/handler_test.go +++ b/internal/handlers/provider_check/handler_test.go @@ -22,14 +22,14 @@ func TestHandler_ProviderCheck(t *testing.T) { name string req *oas.ProviderCheckReq - buildCheckerMock func(m *MockproviderChecker) + buildCheckerMock func(m *MockrulesetChecker) result oas.ProviderCheckRes }{ { name: "ok", req: &oas.ProviderCheckReq{URL: url, Format: format}, - buildCheckerMock: func(m *MockproviderChecker) { + buildCheckerMock: func(m *MockrulesetChecker) { m.EXPECT().Check(gomock.Any(), url, format). Return(entity.RulesetCheckResult{Outcome: entity.RulesetCheckOK, Size: 1024}) }, @@ -38,7 +38,7 @@ func TestHandler_ProviderCheck(t *testing.T) { { name: "http_error", req: &oas.ProviderCheckReq{URL: url, Format: format}, - buildCheckerMock: func(m *MockproviderChecker) { + buildCheckerMock: func(m *MockrulesetChecker) { m.EXPECT().Check(gomock.Any(), url, format). Return(entity.RulesetCheckResult{Outcome: entity.RulesetCheckHTTPError, Status: 404}) }, @@ -47,7 +47,7 @@ func TestHandler_ProviderCheck(t *testing.T) { { name: "empty", req: &oas.ProviderCheckReq{URL: url, Format: format}, - buildCheckerMock: func(m *MockproviderChecker) { + buildCheckerMock: func(m *MockrulesetChecker) { m.EXPECT().Check(gomock.Any(), url, format). Return(entity.RulesetCheckResult{Outcome: entity.RulesetCheckEmpty}) }, @@ -56,7 +56,7 @@ func TestHandler_ProviderCheck(t *testing.T) { { name: "format_mismatch", req: &oas.ProviderCheckReq{URL: url, Format: format}, - buildCheckerMock: func(m *MockproviderChecker) { + buildCheckerMock: func(m *MockrulesetChecker) { m.EXPECT().Check(gomock.Any(), url, format). Return(entity.RulesetCheckResult{Outcome: entity.RulesetCheckFormatMismatch, Size: 512}) }, @@ -65,7 +65,7 @@ func TestHandler_ProviderCheck(t *testing.T) { { name: "unreachable", req: &oas.ProviderCheckReq{URL: url, Format: format}, - buildCheckerMock: func(m *MockproviderChecker) { + buildCheckerMock: func(m *MockrulesetChecker) { m.EXPECT().Check(gomock.Any(), url, format). Return(entity.RulesetCheckResult{Outcome: entity.RulesetCheckUnreachable}) }, @@ -80,7 +80,7 @@ func TestHandler_ProviderCheck(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - checker := NewMockproviderChecker(ctrl) + checker := NewMockrulesetChecker(ctrl) if tc.buildCheckerMock != nil { tc.buildCheckerMock(checker) } diff --git a/internal/handlers/rules/contract.go b/internal/handlers/rules/contract.go index 7402aec..bd9c145 100644 --- a/internal/handlers/rules/contract.go +++ b/internal/handlers/rules/contract.go @@ -1,7 +1,7 @@ //go:generate go tool mockgen -source=contract.go -destination contract_mocks.go -package rules package rules -// ruleFiles serves mirrored rule-provider files from memory. -type ruleFiles interface { +// rulesetMirror serves mirrored rule-provider files from memory. +type rulesetMirror interface { Get(file string) ([]byte, string, bool) } diff --git a/internal/handlers/rules/contract_mocks.go b/internal/handlers/rules/contract_mocks.go index 91e82c9..3216f8b 100644 --- a/internal/handlers/rules/contract_mocks.go +++ b/internal/handlers/rules/contract_mocks.go @@ -15,32 +15,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockruleFiles is a mock of ruleFiles interface. -type MockruleFiles struct { +// MockrulesetMirror is a mock of rulesetMirror interface. +type MockrulesetMirror struct { ctrl *gomock.Controller - recorder *MockruleFilesMockRecorder + recorder *MockrulesetMirrorMockRecorder isgomock struct{} } -// MockruleFilesMockRecorder is the mock recorder for MockruleFiles. -type MockruleFilesMockRecorder struct { - mock *MockruleFiles +// MockrulesetMirrorMockRecorder is the mock recorder for MockrulesetMirror. +type MockrulesetMirrorMockRecorder struct { + mock *MockrulesetMirror } -// NewMockruleFiles creates a new mock instance. -func NewMockruleFiles(ctrl *gomock.Controller) *MockruleFiles { - mock := &MockruleFiles{ctrl: ctrl} - mock.recorder = &MockruleFilesMockRecorder{mock} +// NewMockrulesetMirror creates a new mock instance. +func NewMockrulesetMirror(ctrl *gomock.Controller) *MockrulesetMirror { + mock := &MockrulesetMirror{ctrl: ctrl} + mock.recorder = &MockrulesetMirrorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockruleFiles) EXPECT() *MockruleFilesMockRecorder { +func (m *MockrulesetMirror) EXPECT() *MockrulesetMirrorMockRecorder { return m.recorder } // Get mocks base method. -func (m *MockruleFiles) Get(file string) ([]byte, string, bool) { +func (m *MockrulesetMirror) Get(file string) ([]byte, string, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", file) ret0, _ := ret[0].([]byte) @@ -50,7 +50,7 @@ func (m *MockruleFiles) Get(file string) ([]byte, string, bool) { } // Get indicates an expected call of Get. -func (mr *MockruleFilesMockRecorder) Get(file any) *gomock.Call { +func (mr *MockrulesetMirrorMockRecorder) Get(file any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockruleFiles)(nil).Get), file) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockrulesetMirror)(nil).Get), file) } diff --git a/internal/handlers/rules/handler.go b/internal/handlers/rules/handler.go index 4ed482b..0c46a8a 100644 --- a/internal/handlers/rules/handler.go +++ b/internal/handlers/rules/handler.go @@ -11,11 +11,11 @@ import ( // Handler serves mirrored rule-provider files from memory. type Handler struct { - mirror ruleFiles + mirror rulesetMirror } // New builds the handler. -func New(mirror ruleFiles) *Handler { return &Handler{mirror: mirror} } +func New(mirror rulesetMirror) *Handler { return &Handler{mirror: mirror} } // Rules implements oas.Handler. A missing/unknown file is a 404 (RulesNotFound); a hit // streams the mirrored bytes with nosniff. diff --git a/internal/handlers/rules/handler_test.go b/internal/handlers/rules/handler_test.go index 6626731..596e34c 100644 --- a/internal/handlers/rules/handler_test.go +++ b/internal/handlers/rules/handler_test.go @@ -19,14 +19,14 @@ func TestHandler_Rules(t *testing.T) { name string params oas.RulesParams - buildMirrorMock func(m *MockruleFiles) + buildMirrorMock func(m *MockrulesetMirror) assertFn func(t *testing.T, res oas.RulesRes) }{ { name: "success", params: oas.RulesParams{File: "geosite.yaml"}, - buildMirrorMock: func(m *MockruleFiles) { + buildMirrorMock: func(m *MockrulesetMirror) { m.EXPECT().Get("geosite.yaml").Return(data, "text/yaml", true) }, assertFn: func(t *testing.T, res oas.RulesRes) { @@ -41,7 +41,7 @@ func TestHandler_Rules(t *testing.T) { { name: "notfound.unknown_file", params: oas.RulesParams{File: "missing.yaml"}, - buildMirrorMock: func(m *MockruleFiles) { + buildMirrorMock: func(m *MockrulesetMirror) { m.EXPECT().Get("missing.yaml").Return(nil, "", false) }, assertFn: func(t *testing.T, res oas.RulesRes) { @@ -66,7 +66,7 @@ func TestHandler_Rules(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mirror := NewMockruleFiles(ctrl) + mirror := NewMockrulesetMirror(ctrl) if tc.buildMirrorMock != nil { tc.buildMirrorMock(mirror) } diff --git a/internal/handlers/sub/contract.go b/internal/handlers/sub/contract.go index 2da1bee..f180570 100644 --- a/internal/handlers/sub/contract.go +++ b/internal/handlers/sub/contract.go @@ -8,21 +8,21 @@ import ( "github.com/postlog/subgen/internal/mihomo" ) -// userResolver lists the subscription IDs of service-owned users (for token +// usersRepo lists the subscription IDs of service-owned users (for token // reverse-lookup) and resolves a matched sub_id to its user id. -type userResolver interface { +type usersRepo interface { SubIDs(ctx context.Context) ([]string, error) IDBySubID(ctx context.Context, subID string) (int64, error) } -// fleetReader returns the current (cached) fleet snapshot. -type fleetReader interface { +// fleetService returns the current (cached) fleet snapshot. +type fleetService interface { Fleet(ctx context.Context) (*entity.Fleet, error) } -// configResolver picks the config to render for a subscriber: a user's custom config +// configsRepo picks the config to render for a subscriber: a user's custom config // when present, otherwise the engine's base. -type configResolver interface { +type configsRepo interface { UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) } @@ -36,9 +36,9 @@ type EngineRenderer interface { Render(ctx context.Context, sub *entity.Subscriber, configID int64) ([]byte, RenderMeta, error) } -// mihomoReader reads one config's mihomo content (scoped by config id) for the mihomo +// routingRepo reads one config's mihomo content (scoped by config id) for the mihomo // renderer. -type mihomoReader interface { +type routingRepo interface { Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) diff --git a/internal/handlers/sub/contract_mocks.go b/internal/handlers/sub/contract_mocks.go index def4c2d..ddbac94 100644 --- a/internal/handlers/sub/contract_mocks.go +++ b/internal/handlers/sub/contract_mocks.go @@ -18,32 +18,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockuserResolver is a mock of userResolver interface. -type MockuserResolver struct { +// MockusersRepo is a mock of usersRepo interface. +type MockusersRepo struct { ctrl *gomock.Controller - recorder *MockuserResolverMockRecorder + recorder *MockusersRepoMockRecorder isgomock struct{} } -// MockuserResolverMockRecorder is the mock recorder for MockuserResolver. -type MockuserResolverMockRecorder struct { - mock *MockuserResolver +// MockusersRepoMockRecorder is the mock recorder for MockusersRepo. +type MockusersRepoMockRecorder struct { + mock *MockusersRepo } -// NewMockuserResolver creates a new mock instance. -func NewMockuserResolver(ctrl *gomock.Controller) *MockuserResolver { - mock := &MockuserResolver{ctrl: ctrl} - mock.recorder = &MockuserResolverMockRecorder{mock} +// NewMockusersRepo creates a new mock instance. +func NewMockusersRepo(ctrl *gomock.Controller) *MockusersRepo { + mock := &MockusersRepo{ctrl: ctrl} + mock.recorder = &MockusersRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockuserResolver) EXPECT() *MockuserResolverMockRecorder { +func (m *MockusersRepo) EXPECT() *MockusersRepoMockRecorder { return m.recorder } // IDBySubID mocks base method. -func (m *MockuserResolver) IDBySubID(ctx context.Context, subID string) (int64, error) { +func (m *MockusersRepo) IDBySubID(ctx context.Context, subID string) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IDBySubID", ctx, subID) ret0, _ := ret[0].(int64) @@ -52,13 +52,13 @@ func (m *MockuserResolver) IDBySubID(ctx context.Context, subID string) (int64, } // IDBySubID indicates an expected call of IDBySubID. -func (mr *MockuserResolverMockRecorder) IDBySubID(ctx, subID any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) IDBySubID(ctx, subID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDBySubID", reflect.TypeOf((*MockuserResolver)(nil).IDBySubID), ctx, subID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDBySubID", reflect.TypeOf((*MockusersRepo)(nil).IDBySubID), ctx, subID) } // SubIDs mocks base method. -func (m *MockuserResolver) SubIDs(ctx context.Context) ([]string, error) { +func (m *MockusersRepo) SubIDs(ctx context.Context) ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SubIDs", ctx) ret0, _ := ret[0].([]string) @@ -67,37 +67,37 @@ func (m *MockuserResolver) SubIDs(ctx context.Context) ([]string, error) { } // SubIDs indicates an expected call of SubIDs. -func (mr *MockuserResolverMockRecorder) SubIDs(ctx any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) SubIDs(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubIDs", reflect.TypeOf((*MockuserResolver)(nil).SubIDs), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubIDs", reflect.TypeOf((*MockusersRepo)(nil).SubIDs), ctx) } -// MockfleetReader is a mock of fleetReader interface. -type MockfleetReader struct { +// MockfleetService is a mock of fleetService interface. +type MockfleetService struct { ctrl *gomock.Controller - recorder *MockfleetReaderMockRecorder + recorder *MockfleetServiceMockRecorder isgomock struct{} } -// MockfleetReaderMockRecorder is the mock recorder for MockfleetReader. -type MockfleetReaderMockRecorder struct { - mock *MockfleetReader +// MockfleetServiceMockRecorder is the mock recorder for MockfleetService. +type MockfleetServiceMockRecorder struct { + mock *MockfleetService } -// NewMockfleetReader creates a new mock instance. -func NewMockfleetReader(ctrl *gomock.Controller) *MockfleetReader { - mock := &MockfleetReader{ctrl: ctrl} - mock.recorder = &MockfleetReaderMockRecorder{mock} +// NewMockfleetService creates a new mock instance. +func NewMockfleetService(ctrl *gomock.Controller) *MockfleetService { + mock := &MockfleetService{ctrl: ctrl} + mock.recorder = &MockfleetServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockfleetReader) EXPECT() *MockfleetReaderMockRecorder { +func (m *MockfleetService) EXPECT() *MockfleetServiceMockRecorder { return m.recorder } // Fleet mocks base method. -func (m *MockfleetReader) Fleet(ctx context.Context) (*entity.Fleet, error) { +func (m *MockfleetService) Fleet(ctx context.Context) (*entity.Fleet, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Fleet", ctx) ret0, _ := ret[0].(*entity.Fleet) @@ -106,37 +106,37 @@ func (m *MockfleetReader) Fleet(ctx context.Context) (*entity.Fleet, error) { } // Fleet indicates an expected call of Fleet. -func (mr *MockfleetReaderMockRecorder) Fleet(ctx any) *gomock.Call { +func (mr *MockfleetServiceMockRecorder) Fleet(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fleet", reflect.TypeOf((*MockfleetReader)(nil).Fleet), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fleet", reflect.TypeOf((*MockfleetService)(nil).Fleet), ctx) } -// MockconfigResolver is a mock of configResolver interface. -type MockconfigResolver struct { +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { ctrl *gomock.Controller - recorder *MockconfigResolverMockRecorder + recorder *MockconfigsRepoMockRecorder isgomock struct{} } -// MockconfigResolverMockRecorder is the mock recorder for MockconfigResolver. -type MockconfigResolverMockRecorder struct { - mock *MockconfigResolver +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo } -// NewMockconfigResolver creates a new mock instance. -func NewMockconfigResolver(ctrl *gomock.Controller) *MockconfigResolver { - mock := &MockconfigResolver{ctrl: ctrl} - mock.recorder = &MockconfigResolverMockRecorder{mock} +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockconfigResolver) EXPECT() *MockconfigResolverMockRecorder { +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { return m.recorder } // BaseConfigID mocks base method. -func (m *MockconfigResolver) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) { +func (m *MockconfigsRepo) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BaseConfigID", ctx, kind) ret0, _ := ret[0].(int64) @@ -146,13 +146,13 @@ func (m *MockconfigResolver) BaseConfigID(ctx context.Context, kind entity.Confi } // BaseConfigID indicates an expected call of BaseConfigID. -func (mr *MockconfigResolverMockRecorder) BaseConfigID(ctx, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) BaseConfigID(ctx, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseConfigID", reflect.TypeOf((*MockconfigResolver)(nil).BaseConfigID), ctx, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).BaseConfigID), ctx, kind) } // UserConfigID mocks base method. -func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { +func (m *MockconfigsRepo) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserConfigID", ctx, userID, kind) ret0, _ := ret[0].(int64) @@ -162,9 +162,9 @@ func (m *MockconfigResolver) UserConfigID(ctx context.Context, userID int64, kin } // UserConfigID indicates an expected call of UserConfigID. -func (mr *MockconfigResolverMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { +func (mr *MockconfigsRepoMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigResolver)(nil).UserConfigID), ctx, userID, kind) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigID), ctx, userID, kind) } // MockEngineRenderer is a mock of EngineRenderer interface. @@ -221,32 +221,32 @@ func (mr *MockEngineRendererMockRecorder) Render(ctx, sub, configID any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Render", reflect.TypeOf((*MockEngineRenderer)(nil).Render), ctx, sub, configID) } -// MockmihomoReader is a mock of mihomoReader interface. -type MockmihomoReader struct { +// MockroutingRepo is a mock of routingRepo interface. +type MockroutingRepo struct { ctrl *gomock.Controller - recorder *MockmihomoReaderMockRecorder + recorder *MockroutingRepoMockRecorder isgomock struct{} } -// MockmihomoReaderMockRecorder is the mock recorder for MockmihomoReader. -type MockmihomoReaderMockRecorder struct { - mock *MockmihomoReader +// MockroutingRepoMockRecorder is the mock recorder for MockroutingRepo. +type MockroutingRepoMockRecorder struct { + mock *MockroutingRepo } -// NewMockmihomoReader creates a new mock instance. -func NewMockmihomoReader(ctrl *gomock.Controller) *MockmihomoReader { - mock := &MockmihomoReader{ctrl: ctrl} - mock.recorder = &MockmihomoReaderMockRecorder{mock} +// NewMockroutingRepo creates a new mock instance. +func NewMockroutingRepo(ctrl *gomock.Controller) *MockroutingRepo { + mock := &MockroutingRepo{ctrl: ctrl} + mock.recorder = &MockroutingRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockmihomoReader) EXPECT() *MockmihomoReaderMockRecorder { +func (m *MockroutingRepo) EXPECT() *MockroutingRepoMockRecorder { return m.recorder } // Profile mocks base method. -func (m *MockmihomoReader) Profile(ctx context.Context, configID int64) (mihomo.Profile, error) { +func (m *MockroutingRepo) Profile(ctx context.Context, configID int64) (mihomo.Profile, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Profile", ctx, configID) ret0, _ := ret[0].(mihomo.Profile) @@ -255,13 +255,13 @@ func (m *MockmihomoReader) Profile(ctx context.Context, configID int64) (mihomo. } // Profile indicates an expected call of Profile. -func (mr *MockmihomoReaderMockRecorder) Profile(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Profile(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockmihomoReader)(nil).Profile), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockroutingRepo)(nil).Profile), ctx, configID) } // ProxyGroups mocks base method. -func (m *MockmihomoReader) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) { +func (m *MockroutingRepo) ProxyGroups(ctx context.Context, configID int64) ([]mihomo.ProxyGroup, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ProxyGroups", ctx, configID) ret0, _ := ret[0].([]mihomo.ProxyGroup) @@ -270,13 +270,13 @@ func (m *MockmihomoReader) ProxyGroups(ctx context.Context, configID int64) ([]m } // ProxyGroups indicates an expected call of ProxyGroups. -func (mr *MockmihomoReaderMockRecorder) ProxyGroups(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) ProxyGroups(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProxyGroups", reflect.TypeOf((*MockmihomoReader)(nil).ProxyGroups), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProxyGroups", reflect.TypeOf((*MockroutingRepo)(nil).ProxyGroups), ctx, configID) } // RuleProviders mocks base method. -func (m *MockmihomoReader) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) { +func (m *MockroutingRepo) RuleProviders(ctx context.Context, configID int64) ([]mihomo.RuleProvider, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RuleProviders", ctx, configID) ret0, _ := ret[0].([]mihomo.RuleProvider) @@ -285,13 +285,13 @@ func (m *MockmihomoReader) RuleProviders(ctx context.Context, configID int64) ([ } // RuleProviders indicates an expected call of RuleProviders. -func (mr *MockmihomoReaderMockRecorder) RuleProviders(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) RuleProviders(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleProviders", reflect.TypeOf((*MockmihomoReader)(nil).RuleProviders), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleProviders", reflect.TypeOf((*MockroutingRepo)(nil).RuleProviders), ctx, configID) } // Rules mocks base method. -func (m *MockmihomoReader) Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) { +func (m *MockroutingRepo) Rules(ctx context.Context, configID int64) ([]mihomo.RoutingRule, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Rules", ctx, configID) ret0, _ := ret[0].([]mihomo.RoutingRule) @@ -300,13 +300,13 @@ func (m *MockmihomoReader) Rules(ctx context.Context, configID int64) ([]mihomo. } // Rules indicates an expected call of Rules. -func (mr *MockmihomoReaderMockRecorder) Rules(ctx, configID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Rules(ctx, configID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockmihomoReader)(nil).Rules), ctx, configID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockroutingRepo)(nil).Rules), ctx, configID) } // Setting mocks base method. -func (m *MockmihomoReader) Setting(ctx context.Context, configID int64, key string) (string, error) { +func (m *MockroutingRepo) Setting(ctx context.Context, configID int64, key string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Setting", ctx, configID, key) ret0, _ := ret[0].(string) @@ -315,7 +315,7 @@ func (m *MockmihomoReader) Setting(ctx context.Context, configID int64, key stri } // Setting indicates an expected call of Setting. -func (mr *MockmihomoReaderMockRecorder) Setting(ctx, configID, key any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) Setting(ctx, configID, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setting", reflect.TypeOf((*MockmihomoReader)(nil).Setting), ctx, configID, key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setting", reflect.TypeOf((*MockroutingRepo)(nil).Setting), ctx, configID, key) } diff --git a/internal/handlers/sub/handler.go b/internal/handlers/sub/handler.go index 0331564..43a07bd 100644 --- a/internal/handlers/sub/handler.go +++ b/internal/handlers/sub/handler.go @@ -28,16 +28,16 @@ type RenderMeta struct { // Handler resolves a subscription token to a user, picks their config (custom or // base) for the requested engine, and delegates rendering to that engine's renderer. type Handler struct { - users userResolver - fleet fleetReader - configs configResolver + users usersRepo + fleet fleetService + configs configsRepo renderers map[entity.ConfigKind]EngineRenderer secret string } // New builds the handler. renderers maps each supported engine kind to its renderer. -func New(users userResolver, fleet fleetReader, configs configResolver, renderers map[entity.ConfigKind]EngineRenderer, secret string) *Handler { +func New(users usersRepo, fleet fleetService, configs configsRepo, renderers map[entity.ConfigKind]EngineRenderer, secret string) *Handler { return &Handler{ users: users, fleet: fleet, configs: configs, renderers: renderers, secret: secret, diff --git a/internal/handlers/sub/handler_test.go b/internal/handlers/sub/handler_test.go index b76b0bc..2021e4b 100644 --- a/internal/handlers/sub/handler_test.go +++ b/internal/handlers/sub/handler_test.go @@ -19,9 +19,9 @@ import ( const testSecret = "hmac-secret" type mocks struct { - users *MockuserResolver - fleet *MockfleetReader - configs *MockconfigResolver + users *MockusersRepo + fleet *MockfleetService + configs *MockconfigsRepo renderer *MockEngineRenderer } @@ -108,9 +108,9 @@ func TestHandler_Sub(t *testing.T) { ctrl := gomock.NewController(t) m := &mocks{ - users: NewMockuserResolver(ctrl), - fleet: NewMockfleetReader(ctrl), - configs: NewMockconfigResolver(ctrl), + users: NewMockusersRepo(ctrl), + fleet: NewMockfleetService(ctrl), + configs: NewMockconfigsRepo(ctrl), renderer: NewMockEngineRenderer(ctrl), } if tc.buildMocks != nil { diff --git a/internal/handlers/sub/mihomo_renderer.go b/internal/handlers/sub/mihomo_renderer.go index 7552fbf..8ab5dcc 100644 --- a/internal/handlers/sub/mihomo_renderer.go +++ b/internal/handlers/sub/mihomo_renderer.go @@ -10,14 +10,14 @@ import ( // MihomoRenderer is the mihomo (Clash.Meta) engineRenderer: it loads the config's // mihomo content (scoped by config id) and renders the per-subscriber YAML profile. type MihomoRenderer struct { - routing mihomoReader + routing routingRepo publicBase string } // NewMihomoRenderer builds the mihomo renderer. publicBase rewrites mirrored // rule-provider URLs; the response metadata (filename, title, update interval) is read // per-config from the store at render time. -func NewMihomoRenderer(routing mihomoReader, publicBase string) *MihomoRenderer { +func NewMihomoRenderer(routing routingRepo, publicBase string) *MihomoRenderer { return &MihomoRenderer{routing: routing, publicBase: publicBase} } diff --git a/internal/handlers/user_create/contract.go b/internal/handlers/user_create/contract.go index f30bfe1..966985a 100644 --- a/internal/handlers/user_create/contract.go +++ b/internal/handlers/user_create/contract.go @@ -7,7 +7,7 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// creator provisions a new user (the provisioning service satisfies it). -type creator interface { +// provisioningService provisions a new user (the provisioning service satisfies it). +type provisioningService interface { CreateUser(ctx context.Context, p entity.UserCreateParams) (*entity.User, error) } diff --git a/internal/handlers/user_create/contract_mocks.go b/internal/handlers/user_create/contract_mocks.go index 3107263..c6fb375 100644 --- a/internal/handlers/user_create/contract_mocks.go +++ b/internal/handlers/user_create/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// Mockcreator is a mock of creator interface. -type Mockcreator struct { +// MockprovisioningService is a mock of provisioningService interface. +type MockprovisioningService struct { ctrl *gomock.Controller - recorder *MockcreatorMockRecorder + recorder *MockprovisioningServiceMockRecorder isgomock struct{} } -// MockcreatorMockRecorder is the mock recorder for Mockcreator. -type MockcreatorMockRecorder struct { - mock *Mockcreator +// MockprovisioningServiceMockRecorder is the mock recorder for MockprovisioningService. +type MockprovisioningServiceMockRecorder struct { + mock *MockprovisioningService } -// NewMockcreator creates a new mock instance. -func NewMockcreator(ctrl *gomock.Controller) *Mockcreator { - mock := &Mockcreator{ctrl: ctrl} - mock.recorder = &MockcreatorMockRecorder{mock} +// NewMockprovisioningService creates a new mock instance. +func NewMockprovisioningService(ctrl *gomock.Controller) *MockprovisioningService { + mock := &MockprovisioningService{ctrl: ctrl} + mock.recorder = &MockprovisioningServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockcreator) EXPECT() *MockcreatorMockRecorder { +func (m *MockprovisioningService) EXPECT() *MockprovisioningServiceMockRecorder { return m.recorder } // CreateUser mocks base method. -func (m *Mockcreator) CreateUser(ctx context.Context, p entity.UserCreateParams) (*entity.User, error) { +func (m *MockprovisioningService) CreateUser(ctx context.Context, p entity.UserCreateParams) (*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateUser", ctx, p) ret0, _ := ret[0].(*entity.User) @@ -51,7 +51,7 @@ func (m *Mockcreator) CreateUser(ctx context.Context, p entity.UserCreateParams) } // CreateUser indicates an expected call of CreateUser. -func (mr *MockcreatorMockRecorder) CreateUser(ctx, p any) *gomock.Call { +func (mr *MockprovisioningServiceMockRecorder) CreateUser(ctx, p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*Mockcreator)(nil).CreateUser), ctx, p) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockprovisioningService)(nil).CreateUser), ctx, p) } diff --git a/internal/handlers/user_create/handler.go b/internal/handlers/user_create/handler.go index 57b38ce..9bf31e4 100644 --- a/internal/handlers/user_create/handler.go +++ b/internal/handlers/user_create/handler.go @@ -24,11 +24,11 @@ const ( // Handler provisions a new user. type Handler struct { - svc creator + svc provisioningService } // New builds the handler. -func New(svc creator) *Handler { return &Handler{svc: svc} } +func New(svc provisioningService) *Handler { return &Handler{svc: svc} } // UserCreate implements oas.Handler: a nickname clash (or a panel client clash) is a // 409, other invalid input is a 400, and any unexpected (infra) failure is a 500. diff --git a/internal/handlers/user_create/handler_test.go b/internal/handlers/user_create/handler_test.go index f12ec12..fb5baf3 100644 --- a/internal/handlers/user_create/handler_test.go +++ b/internal/handlers/user_create/handler_test.go @@ -26,7 +26,7 @@ func TestHandler_UserCreate(t *testing.T) { name string req *oas.UserCreateReq - buildCreatorMock func(m *Mockcreator) + buildCreatorMock func(m *MockprovisioningService) result oas.UserCreateRes err error @@ -34,7 +34,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "success", req: &oas.UserCreateReq{Name: "alice", Description: oas.NewOptString("заметка"), InboundIDs: []int64{1, 2}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), entity.UserCreateParams{ Name: "alice", Description: utils.Ptr("заметка"), InboundIDs: []int64{1, 2}, }).Return(&entity.User{ID: 7}, nil) @@ -44,7 +44,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.name_taken", req: &oas.UserCreateReq{Name: "bob", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("bob", 1)).Return(nil, entity.ErrNameTaken) }, result: &oas.UserCreateConflict{ErrMessage: MsgNameTaken}, @@ -52,7 +52,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.panel_client_exists", req: &oas.UserCreateReq{Name: "bob", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("bob", 1)).Return(nil, entity.PanelClientExistsError{Node: "N1"}) }, result: &oas.UserCreateConflict{ErrMessage: "на панели «N1» уже есть клиент с таким именем — удалите его там вручную или выберите другое имя"}, @@ -60,7 +60,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.invalid_name", req: &oas.UserCreateReq{Name: "bad name", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("bad name", 1)).Return(nil, entity.ErrInvalidUserName) }, result: &oas.UserCreateBadRequest{ErrMessage: MsgInvalidName}, @@ -68,7 +68,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.no_connection", req: &oas.UserCreateReq{Name: "carol", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("carol", 1)).Return(nil, entity.ErrNoConnectionSelected) }, result: &oas.UserCreateBadRequest{ErrMessage: MsgNoConnection}, @@ -76,7 +76,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.description_too_long", req: &oas.UserCreateReq{Name: "carol", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("carol", 1)).Return(nil, entity.ErrDescriptionTooLong) }, result: &oas.UserCreateBadRequest{ErrMessage: MsgDescTooLong}, @@ -84,7 +84,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.inbound_not_found", req: &oas.UserCreateReq{Name: "carol", InboundIDs: []int64{99}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("carol", 99)).Return(nil, entity.ErrInboundNotFound) }, result: &oas.UserCreateBadRequest{ErrMessage: MsgInboundNotFound}, @@ -92,7 +92,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.node_not_found", req: &oas.UserCreateReq{Name: "carol", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("carol", 1)).Return(nil, entity.ErrNodeNotFound) }, result: &oas.UserCreateBadRequest{ErrMessage: MsgNodeNotFound}, @@ -100,7 +100,7 @@ func TestHandler_UserCreate(t *testing.T) { { name: "error.internal", req: &oas.UserCreateReq{Name: "dave", InboundIDs: []int64{1}}, - buildCreatorMock: func(m *Mockcreator) { + buildCreatorMock: func(m *MockprovisioningService) { m.EXPECT().CreateUser(gomock.Any(), params("dave", 1)).Return(nil, internalErr) }, err: internalErr, @@ -114,7 +114,7 @@ func TestHandler_UserCreate(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMockcreator(ctrl) + svc := NewMockprovisioningService(ctrl) if tc.buildCreatorMock != nil { tc.buildCreatorMock(svc) } diff --git a/internal/handlers/user_delete/contract.go b/internal/handlers/user_delete/contract.go index c1124c2..3448062 100644 --- a/internal/handlers/user_delete/contract.go +++ b/internal/handlers/user_delete/contract.go @@ -3,7 +3,7 @@ package user_delete import "context" -// deleter removes a user (the provisioning service satisfies it). -type deleter interface { +// provisioningService removes a user (the provisioning service satisfies it). +type provisioningService interface { DeleteUser(ctx context.Context, id int64) error } diff --git a/internal/handlers/user_delete/contract_mocks.go b/internal/handlers/user_delete/contract_mocks.go index 8398b00..2f1fd8a 100644 --- a/internal/handlers/user_delete/contract_mocks.go +++ b/internal/handlers/user_delete/contract_mocks.go @@ -16,32 +16,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// Mockdeleter is a mock of deleter interface. -type Mockdeleter struct { +// MockprovisioningService is a mock of provisioningService interface. +type MockprovisioningService struct { ctrl *gomock.Controller - recorder *MockdeleterMockRecorder + recorder *MockprovisioningServiceMockRecorder isgomock struct{} } -// MockdeleterMockRecorder is the mock recorder for Mockdeleter. -type MockdeleterMockRecorder struct { - mock *Mockdeleter +// MockprovisioningServiceMockRecorder is the mock recorder for MockprovisioningService. +type MockprovisioningServiceMockRecorder struct { + mock *MockprovisioningService } -// NewMockdeleter creates a new mock instance. -func NewMockdeleter(ctrl *gomock.Controller) *Mockdeleter { - mock := &Mockdeleter{ctrl: ctrl} - mock.recorder = &MockdeleterMockRecorder{mock} +// NewMockprovisioningService creates a new mock instance. +func NewMockprovisioningService(ctrl *gomock.Controller) *MockprovisioningService { + mock := &MockprovisioningService{ctrl: ctrl} + mock.recorder = &MockprovisioningServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockdeleter) EXPECT() *MockdeleterMockRecorder { +func (m *MockprovisioningService) EXPECT() *MockprovisioningServiceMockRecorder { return m.recorder } // DeleteUser mocks base method. -func (m *Mockdeleter) DeleteUser(ctx context.Context, id int64) error { +func (m *MockprovisioningService) DeleteUser(ctx context.Context, id int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteUser", ctx, id) ret0, _ := ret[0].(error) @@ -49,7 +49,7 @@ func (m *Mockdeleter) DeleteUser(ctx context.Context, id int64) error { } // DeleteUser indicates an expected call of DeleteUser. -func (mr *MockdeleterMockRecorder) DeleteUser(ctx, id any) *gomock.Call { +func (mr *MockprovisioningServiceMockRecorder) DeleteUser(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*Mockdeleter)(nil).DeleteUser), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockprovisioningService)(nil).DeleteUser), ctx, id) } diff --git a/internal/handlers/user_delete/handler.go b/internal/handlers/user_delete/handler.go index 994e2be..a8c5a6d 100644 --- a/internal/handlers/user_delete/handler.go +++ b/internal/handlers/user_delete/handler.go @@ -14,11 +14,11 @@ const MsgDeleted = "Пользователь удалён" // Handler deletes a user and deprovisions its panel clients. type Handler struct { - svc deleter + svc provisioningService } // New builds the handler. -func New(svc deleter) *Handler { return &Handler{svc: svc} } +func New(svc provisioningService) *Handler { return &Handler{svc: svc} } // UserDelete implements oas.Handler. DeleteUser surfaces no domain (4xx) error — a missing // user or a panel/store failure is an internal condition — so any failure is a logged 500. diff --git a/internal/handlers/user_delete/handler_test.go b/internal/handlers/user_delete/handler_test.go index 88c9b92..87b469e 100644 --- a/internal/handlers/user_delete/handler_test.go +++ b/internal/handlers/user_delete/handler_test.go @@ -19,7 +19,7 @@ func TestHandler_UserDelete(t *testing.T) { name string req *oas.UserDeleteReq - buildDeleterMock func(m *Mockdeleter) + buildDeleterMock func(m *MockprovisioningService) result oas.UserDeleteRes err error @@ -27,7 +27,7 @@ func TestHandler_UserDelete(t *testing.T) { { name: "success", req: &oas.UserDeleteReq{ID: 7}, - buildDeleterMock: func(m *Mockdeleter) { + buildDeleterMock: func(m *MockprovisioningService) { m.EXPECT().DeleteUser(gomock.Any(), int64(7)).Return(nil) }, result: &oas.MessageResponse{Message: MsgDeleted}, @@ -35,7 +35,7 @@ func TestHandler_UserDelete(t *testing.T) { { name: "error.internal", req: &oas.UserDeleteReq{ID: 7}, - buildDeleterMock: func(m *Mockdeleter) { + buildDeleterMock: func(m *MockprovisioningService) { m.EXPECT().DeleteUser(gomock.Any(), int64(7)).Return(internalErr) }, err: internalErr, @@ -49,7 +49,7 @@ func TestHandler_UserDelete(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMockdeleter(ctrl) + svc := NewMockprovisioningService(ctrl) if tc.buildDeleterMock != nil { tc.buildDeleterMock(svc) } diff --git a/internal/handlers/user_edit/contract.go b/internal/handlers/user_edit/contract.go index 8f8dd19..3248d7a 100644 --- a/internal/handlers/user_edit/contract.go +++ b/internal/handlers/user_edit/contract.go @@ -7,8 +7,8 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// editor updates a user's connections and description (the provisioning service +// provisioningService updates a user's connections and description (the provisioning service // satisfies it). -type editor interface { +type provisioningService interface { EditUser(ctx context.Context, p entity.UserEditParams) error } diff --git a/internal/handlers/user_edit/contract_mocks.go b/internal/handlers/user_edit/contract_mocks.go index 0159cda..cb65691 100644 --- a/internal/handlers/user_edit/contract_mocks.go +++ b/internal/handlers/user_edit/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// Mockeditor is a mock of editor interface. -type Mockeditor struct { +// MockprovisioningService is a mock of provisioningService interface. +type MockprovisioningService struct { ctrl *gomock.Controller - recorder *MockeditorMockRecorder + recorder *MockprovisioningServiceMockRecorder isgomock struct{} } -// MockeditorMockRecorder is the mock recorder for Mockeditor. -type MockeditorMockRecorder struct { - mock *Mockeditor +// MockprovisioningServiceMockRecorder is the mock recorder for MockprovisioningService. +type MockprovisioningServiceMockRecorder struct { + mock *MockprovisioningService } -// NewMockeditor creates a new mock instance. -func NewMockeditor(ctrl *gomock.Controller) *Mockeditor { - mock := &Mockeditor{ctrl: ctrl} - mock.recorder = &MockeditorMockRecorder{mock} +// NewMockprovisioningService creates a new mock instance. +func NewMockprovisioningService(ctrl *gomock.Controller) *MockprovisioningService { + mock := &MockprovisioningService{ctrl: ctrl} + mock.recorder = &MockprovisioningServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockeditor) EXPECT() *MockeditorMockRecorder { +func (m *MockprovisioningService) EXPECT() *MockprovisioningServiceMockRecorder { return m.recorder } // EditUser mocks base method. -func (m *Mockeditor) EditUser(ctx context.Context, p entity.UserEditParams) error { +func (m *MockprovisioningService) EditUser(ctx context.Context, p entity.UserEditParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EditUser", ctx, p) ret0, _ := ret[0].(error) @@ -50,7 +50,7 @@ func (m *Mockeditor) EditUser(ctx context.Context, p entity.UserEditParams) erro } // EditUser indicates an expected call of EditUser. -func (mr *MockeditorMockRecorder) EditUser(ctx, p any) *gomock.Call { +func (mr *MockprovisioningServiceMockRecorder) EditUser(ctx, p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditUser", reflect.TypeOf((*Mockeditor)(nil).EditUser), ctx, p) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditUser", reflect.TypeOf((*MockprovisioningService)(nil).EditUser), ctx, p) } diff --git a/internal/handlers/user_edit/handler.go b/internal/handlers/user_edit/handler.go index 15794b2..a434d3d 100644 --- a/internal/handlers/user_edit/handler.go +++ b/internal/handlers/user_edit/handler.go @@ -21,11 +21,11 @@ const ( // Handler re-binds a user to a new inbound set. type Handler struct { - svc editor + svc provisioningService } // New builds the handler. -func New(svc editor) *Handler { return &Handler{svc: svc} } +func New(svc provisioningService) *Handler { return &Handler{svc: svc} } // UserEdit implements oas.Handler: invalid input is a 400, any unexpected (infra) // failure is a 500. diff --git a/internal/handlers/user_edit/handler_test.go b/internal/handlers/user_edit/handler_test.go index c1773a1..d28c0a6 100644 --- a/internal/handlers/user_edit/handler_test.go +++ b/internal/handlers/user_edit/handler_test.go @@ -26,7 +26,7 @@ func TestHandler_UserEdit(t *testing.T) { name string req *oas.UserEditReq - buildEditorMock func(m *Mockeditor) + buildEditorMock func(m *MockprovisioningService) result oas.UserEditRes err error @@ -34,7 +34,7 @@ func TestHandler_UserEdit(t *testing.T) { { name: "success", req: &oas.UserEditReq{ID: 7, Description: oas.NewOptString("заметка"), InboundIDs: []int64{1, 2}}, - buildEditorMock: func(m *Mockeditor) { + buildEditorMock: func(m *MockprovisioningService) { m.EXPECT().EditUser(gomock.Any(), entity.UserEditParams{ ID: 7, Description: utils.Ptr("заметка"), InboundIDs: []int64{1, 2}, }).Return(nil) @@ -44,7 +44,7 @@ func TestHandler_UserEdit(t *testing.T) { { name: "error.no_connection", req: &oas.UserEditReq{ID: 7, InboundIDs: []int64{}}, - buildEditorMock: func(m *Mockeditor) { + buildEditorMock: func(m *MockprovisioningService) { m.EXPECT().EditUser(gomock.Any(), entity.UserEditParams{ID: 7, InboundIDs: []int64{}}).Return(entity.ErrNoConnectionSelected) }, result: &oas.UserEditBadRequest{ErrMessage: MsgNoConnection}, @@ -52,7 +52,7 @@ func TestHandler_UserEdit(t *testing.T) { { name: "error.inbound_not_found", req: &oas.UserEditReq{ID: 7, InboundIDs: []int64{99}}, - buildEditorMock: func(m *Mockeditor) { + buildEditorMock: func(m *MockprovisioningService) { m.EXPECT().EditUser(gomock.Any(), editParams(7, 99)).Return(entity.ErrInboundNotFound) }, result: &oas.UserEditBadRequest{ErrMessage: MsgInboundNotFound}, @@ -60,7 +60,7 @@ func TestHandler_UserEdit(t *testing.T) { { name: "error.description_too_long", req: &oas.UserEditReq{ID: 7, InboundIDs: []int64{1}}, - buildEditorMock: func(m *Mockeditor) { + buildEditorMock: func(m *MockprovisioningService) { m.EXPECT().EditUser(gomock.Any(), editParams(7, 1)).Return(entity.ErrDescriptionTooLong) }, result: &oas.UserEditBadRequest{ErrMessage: MsgDescTooLong}, @@ -68,7 +68,7 @@ func TestHandler_UserEdit(t *testing.T) { { name: "error.internal", req: &oas.UserEditReq{ID: 7, InboundIDs: []int64{1}}, - buildEditorMock: func(m *Mockeditor) { + buildEditorMock: func(m *MockprovisioningService) { m.EXPECT().EditUser(gomock.Any(), editParams(7, 1)).Return(internalErr) }, err: internalErr, @@ -82,7 +82,7 @@ func TestHandler_UserEdit(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMockeditor(ctrl) + svc := NewMockprovisioningService(ctrl) if tc.buildEditorMock != nil { tc.buildEditorMock(svc) } diff --git a/internal/handlers/user_recreate/contract.go b/internal/handlers/user_recreate/contract.go index 3b5fb03..839f455 100644 --- a/internal/handlers/user_recreate/contract.go +++ b/internal/handlers/user_recreate/contract.go @@ -3,7 +3,7 @@ package user_recreate import "context" -// recreator re-provisions a user's panel clients (the provisioning service satisfies it). -type recreator interface { +// provisioningService re-provisions a user's panel clients (the provisioning service satisfies it). +type provisioningService interface { RecreateUser(ctx context.Context, id int64) error } diff --git a/internal/handlers/user_recreate/contract_mocks.go b/internal/handlers/user_recreate/contract_mocks.go index 00f98d8..ef20167 100644 --- a/internal/handlers/user_recreate/contract_mocks.go +++ b/internal/handlers/user_recreate/contract_mocks.go @@ -16,32 +16,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// Mockrecreator is a mock of recreator interface. -type Mockrecreator struct { +// MockprovisioningService is a mock of provisioningService interface. +type MockprovisioningService struct { ctrl *gomock.Controller - recorder *MockrecreatorMockRecorder + recorder *MockprovisioningServiceMockRecorder isgomock struct{} } -// MockrecreatorMockRecorder is the mock recorder for Mockrecreator. -type MockrecreatorMockRecorder struct { - mock *Mockrecreator +// MockprovisioningServiceMockRecorder is the mock recorder for MockprovisioningService. +type MockprovisioningServiceMockRecorder struct { + mock *MockprovisioningService } -// NewMockrecreator creates a new mock instance. -func NewMockrecreator(ctrl *gomock.Controller) *Mockrecreator { - mock := &Mockrecreator{ctrl: ctrl} - mock.recorder = &MockrecreatorMockRecorder{mock} +// NewMockprovisioningService creates a new mock instance. +func NewMockprovisioningService(ctrl *gomock.Controller) *MockprovisioningService { + mock := &MockprovisioningService{ctrl: ctrl} + mock.recorder = &MockprovisioningServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockrecreator) EXPECT() *MockrecreatorMockRecorder { +func (m *MockprovisioningService) EXPECT() *MockprovisioningServiceMockRecorder { return m.recorder } // RecreateUser mocks base method. -func (m *Mockrecreator) RecreateUser(ctx context.Context, id int64) error { +func (m *MockprovisioningService) RecreateUser(ctx context.Context, id int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RecreateUser", ctx, id) ret0, _ := ret[0].(error) @@ -49,7 +49,7 @@ func (m *Mockrecreator) RecreateUser(ctx context.Context, id int64) error { } // RecreateUser indicates an expected call of RecreateUser. -func (mr *MockrecreatorMockRecorder) RecreateUser(ctx, id any) *gomock.Call { +func (mr *MockprovisioningServiceMockRecorder) RecreateUser(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecreateUser", reflect.TypeOf((*Mockrecreator)(nil).RecreateUser), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecreateUser", reflect.TypeOf((*MockprovisioningService)(nil).RecreateUser), ctx, id) } diff --git a/internal/handlers/user_recreate/handler.go b/internal/handlers/user_recreate/handler.go index f08b054..4f2ba36 100644 --- a/internal/handlers/user_recreate/handler.go +++ b/internal/handlers/user_recreate/handler.go @@ -15,11 +15,11 @@ const MsgRecreated = "Клиенты пересозданы" // Handler re-provisions a user's panel clients from the store. type Handler struct { - svc recreator + svc provisioningService } // New builds the handler. -func New(svc recreator) *Handler { return &Handler{svc: svc} } +func New(svc provisioningService) *Handler { return &Handler{svc: svc} } // UserRecreate implements oas.Handler. RecreateUser surfaces no domain (4xx) error — a // missing user or a panel/store failure is an internal condition — so any failure is a diff --git a/internal/handlers/user_recreate/handler_test.go b/internal/handlers/user_recreate/handler_test.go index 2d2b22d..4043c99 100644 --- a/internal/handlers/user_recreate/handler_test.go +++ b/internal/handlers/user_recreate/handler_test.go @@ -19,7 +19,7 @@ func TestHandler_UserRecreate(t *testing.T) { name string req *oas.UserRecreateReq - buildRecreatorMock func(m *Mockrecreator) + buildRecreatorMock func(m *MockprovisioningService) result oas.UserRecreateRes err error @@ -27,7 +27,7 @@ func TestHandler_UserRecreate(t *testing.T) { { name: "success", req: &oas.UserRecreateReq{ID: 7}, - buildRecreatorMock: func(m *Mockrecreator) { + buildRecreatorMock: func(m *MockprovisioningService) { m.EXPECT().RecreateUser(gomock.Any(), int64(7)).Return(nil) }, result: &oas.MessageResponse{Message: MsgRecreated}, @@ -35,7 +35,7 @@ func TestHandler_UserRecreate(t *testing.T) { { name: "error.internal", req: &oas.UserRecreateReq{ID: 7}, - buildRecreatorMock: func(m *Mockrecreator) { + buildRecreatorMock: func(m *MockprovisioningService) { m.EXPECT().RecreateUser(gomock.Any(), int64(7)).Return(internalErr) }, err: internalErr, @@ -49,7 +49,7 @@ func TestHandler_UserRecreate(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - svc := NewMockrecreator(ctrl) + svc := NewMockprovisioningService(ctrl) if tc.buildRecreatorMock != nil { tc.buildRecreatorMock(svc) } diff --git a/internal/handlers/users_get/contract.go b/internal/handlers/users_get/contract.go index a522b73..244405a 100644 --- a/internal/handlers/users_get/contract.go +++ b/internal/handlers/users_get/contract.go @@ -7,10 +7,14 @@ import ( "github.com/postlog/subgen/internal/entity" ) -type userLister interface { +type usersRepo interface { ListPage(ctx context.Context, p entity.UserListParams) (entity.UserPage, error) } -type fleetReader interface { +type fleetService interface { Fleet(ctx context.Context) (*entity.Fleet, error) } + +type sublinksService interface { + Links(ctx context.Context, users []entity.User) (map[int64][]entity.SubLink, error) +} diff --git a/internal/handlers/users_get/contract_mocks.go b/internal/handlers/users_get/contract_mocks.go index 9108450..fa528db 100644 --- a/internal/handlers/users_get/contract_mocks.go +++ b/internal/handlers/users_get/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockuserLister is a mock of userLister interface. -type MockuserLister struct { +// MockusersRepo is a mock of usersRepo interface. +type MockusersRepo struct { ctrl *gomock.Controller - recorder *MockuserListerMockRecorder + recorder *MockusersRepoMockRecorder isgomock struct{} } -// MockuserListerMockRecorder is the mock recorder for MockuserLister. -type MockuserListerMockRecorder struct { - mock *MockuserLister +// MockusersRepoMockRecorder is the mock recorder for MockusersRepo. +type MockusersRepoMockRecorder struct { + mock *MockusersRepo } -// NewMockuserLister creates a new mock instance. -func NewMockuserLister(ctrl *gomock.Controller) *MockuserLister { - mock := &MockuserLister{ctrl: ctrl} - mock.recorder = &MockuserListerMockRecorder{mock} +// NewMockusersRepo creates a new mock instance. +func NewMockusersRepo(ctrl *gomock.Controller) *MockusersRepo { + mock := &MockusersRepo{ctrl: ctrl} + mock.recorder = &MockusersRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockuserLister) EXPECT() *MockuserListerMockRecorder { +func (m *MockusersRepo) EXPECT() *MockusersRepoMockRecorder { return m.recorder } // ListPage mocks base method. -func (m *MockuserLister) ListPage(ctx context.Context, p entity.UserListParams) (entity.UserPage, error) { +func (m *MockusersRepo) ListPage(ctx context.Context, p entity.UserListParams) (entity.UserPage, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPage", ctx, p) ret0, _ := ret[0].(entity.UserPage) @@ -51,37 +51,37 @@ func (m *MockuserLister) ListPage(ctx context.Context, p entity.UserListParams) } // ListPage indicates an expected call of ListPage. -func (mr *MockuserListerMockRecorder) ListPage(ctx, p any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) ListPage(ctx, p any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPage", reflect.TypeOf((*MockuserLister)(nil).ListPage), ctx, p) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPage", reflect.TypeOf((*MockusersRepo)(nil).ListPage), ctx, p) } -// MockfleetReader is a mock of fleetReader interface. -type MockfleetReader struct { +// MockfleetService is a mock of fleetService interface. +type MockfleetService struct { ctrl *gomock.Controller - recorder *MockfleetReaderMockRecorder + recorder *MockfleetServiceMockRecorder isgomock struct{} } -// MockfleetReaderMockRecorder is the mock recorder for MockfleetReader. -type MockfleetReaderMockRecorder struct { - mock *MockfleetReader +// MockfleetServiceMockRecorder is the mock recorder for MockfleetService. +type MockfleetServiceMockRecorder struct { + mock *MockfleetService } -// NewMockfleetReader creates a new mock instance. -func NewMockfleetReader(ctrl *gomock.Controller) *MockfleetReader { - mock := &MockfleetReader{ctrl: ctrl} - mock.recorder = &MockfleetReaderMockRecorder{mock} +// NewMockfleetService creates a new mock instance. +func NewMockfleetService(ctrl *gomock.Controller) *MockfleetService { + mock := &MockfleetService{ctrl: ctrl} + mock.recorder = &MockfleetServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockfleetReader) EXPECT() *MockfleetReaderMockRecorder { +func (m *MockfleetService) EXPECT() *MockfleetServiceMockRecorder { return m.recorder } // Fleet mocks base method. -func (m *MockfleetReader) Fleet(ctx context.Context) (*entity.Fleet, error) { +func (m *MockfleetService) Fleet(ctx context.Context) (*entity.Fleet, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Fleet", ctx) ret0, _ := ret[0].(*entity.Fleet) @@ -90,7 +90,46 @@ func (m *MockfleetReader) Fleet(ctx context.Context) (*entity.Fleet, error) { } // Fleet indicates an expected call of Fleet. -func (mr *MockfleetReaderMockRecorder) Fleet(ctx any) *gomock.Call { +func (mr *MockfleetServiceMockRecorder) Fleet(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fleet", reflect.TypeOf((*MockfleetReader)(nil).Fleet), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fleet", reflect.TypeOf((*MockfleetService)(nil).Fleet), ctx) +} + +// MocksublinksService is a mock of sublinksService interface. +type MocksublinksService struct { + ctrl *gomock.Controller + recorder *MocksublinksServiceMockRecorder + isgomock struct{} +} + +// MocksublinksServiceMockRecorder is the mock recorder for MocksublinksService. +type MocksublinksServiceMockRecorder struct { + mock *MocksublinksService +} + +// NewMocksublinksService creates a new mock instance. +func NewMocksublinksService(ctrl *gomock.Controller) *MocksublinksService { + mock := &MocksublinksService{ctrl: ctrl} + mock.recorder = &MocksublinksServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MocksublinksService) EXPECT() *MocksublinksServiceMockRecorder { + return m.recorder +} + +// Links mocks base method. +func (m *MocksublinksService) Links(ctx context.Context, users []entity.User) (map[int64][]entity.SubLink, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Links", ctx, users) + ret0, _ := ret[0].(map[int64][]entity.SubLink) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Links indicates an expected call of Links. +func (mr *MocksublinksServiceMockRecorder) Links(ctx, users any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Links", reflect.TypeOf((*MocksublinksService)(nil).Links), ctx, users) } diff --git a/internal/handlers/users_get/handler.go b/internal/handlers/users_get/handler.go index d417f87..4dc6d8c 100644 --- a/internal/handlers/users_get/handler.go +++ b/internal/handlers/users_get/handler.go @@ -5,11 +5,9 @@ package users_get import ( "context" "log/slog" - "strings" "github.com/postlog/subgen/internal/entity" "github.com/postlog/subgen/internal/oas" - "github.com/postlog/subgen/internal/token" ) const ( @@ -19,15 +17,14 @@ const ( // Handler serves a page of the users list. type Handler struct { - users userLister - fleet fleetReader - secret string // HMAC secret for subscription tokens - base string // public base URL + users usersRepo + fleet fleetService + links sublinksService } // New builds the handler. -func New(users userLister, fleet fleetReader, secret, base string) *Handler { - return &Handler{users: users, fleet: fleet, secret: secret, base: base} +func New(users usersRepo, fleet fleetService, links sublinksService) *Handler { + return &Handler{users: users, fleet: fleet, links: links} } // UsersGet implements oas.Handler. Health badges and traffic come from the cached @@ -57,7 +54,12 @@ func (h *Handler) UsersGet(ctx context.Context, params oas.UsersGetParams) (oas. } fl, _ := h.fleet.Fleet(ctx) - base := strings.TrimRight(h.base, "/") + + links, err := h.links.Links(ctx, res.Users) + if err != nil { + slog.Error("handler users_get: build subscription links failed", "page", page, "perPage", perPage, "err", err) + return nil, err + } rows := make([]oas.UsersGetOKUsersItem, 0, len(res.Users)) @@ -72,9 +74,14 @@ func (h *Handler) UsersGet(ctx context.Context, params oas.UsersGetParams) (oas. }) } + sub := make([]oas.UsersGetOKUsersItemSubLinksItem, 0, len(links[u.ID])) + for _, l := range links[u.ID] { + sub = append(sub, oas.UsersGetOKUsersItemSubLinksItem{Title: l.Title, Value: l.Value}) + } + row := oas.UsersGetOKUsersItem{ ID: u.ID, Name: u.Name, Inbounds: inbounds, - Sub: oas.UsersGetOKUsersItemSub{ID: u.SubID, URL: base + "/sub/mihomo/" + token.Make(h.secret, u.SubID)}, + Sub: oas.UsersGetOKUsersItemSub{Links: sub}, } if u.Description != nil { diff --git a/internal/handlers/users_get/handler_test.go b/internal/handlers/users_get/handler_test.go index 7a02758..3b5916b 100644 --- a/internal/handlers/users_get/handler_test.go +++ b/internal/handlers/users_get/handler_test.go @@ -11,15 +11,10 @@ import ( "github.com/postlog/subgen/internal/entity" "github.com/postlog/subgen/internal/oas" - "github.com/postlog/subgen/internal/token" ) func TestHandler_UsersGet(t *testing.T) { - const ( - secret = "secret" - base = "http://base" - subID = "sub-1" - ) + const subID = "sub-1" internalErr := errors.New("db down") @@ -27,8 +22,9 @@ func TestHandler_UsersGet(t *testing.T) { name string params oas.UsersGetParams - buildUsersMock func(m *MockuserLister) - buildFleetMock func(m *MockfleetReader) + buildUsersMock func(m *MockusersRepo) + buildFleetMock func(m *MockfleetService) + buildLinksMock func(m *MocksublinksService) result oas.UsersGetRes err error @@ -36,7 +32,7 @@ func TestHandler_UsersGet(t *testing.T) { { name: "success", params: oas.UsersGetParams{}, - buildUsersMock: func(m *MockuserLister) { + buildUsersMock: func(m *MockusersRepo) { m.EXPECT(). ListPage(gomock.Any(), entity.UserListParams{Limit: 50, Offset: 0}). Return(entity.UserPage{ @@ -51,16 +47,26 @@ func TestHandler_UsersGet(t *testing.T) { Total: 1, }, nil) }, - buildFleetMock: func(m *MockfleetReader) { + buildFleetMock: func(m *MockfleetService) { m.EXPECT().Fleet(gomock.Any()).Return(&entity.Fleet{}, nil) }, + buildLinksMock: func(m *MocksublinksService) { + m.EXPECT().Links(gomock.Any(), gomock.Any()).Return(map[int64][]entity.SubLink{ + 7: { + {Title: "Mihomo", Value: "http://base/sub/mihomo/tok"}, + {Title: "Clashmi", Value: "clashmi://install-config?url=enc&name=Freedom&overwrite=false"}, + }, + }, nil) + }, result: &oas.UsersGetOK{ Users: []oas.UsersGetOKUsersItem{{ ID: 7, Name: "alice", Sub: oas.UsersGetOKUsersItemSub{ - ID: subID, - URL: base + "/sub/mihomo/" + token.Make(secret, subID), + Links: []oas.UsersGetOKUsersItemSubLinksItem{ + {Title: "Mihomo", Value: "http://base/sub/mihomo/tok"}, + {Title: "Clashmi", Value: "clashmi://install-config?url=enc&name=Freedom&overwrite=false"}, + }, }, Inbounds: []oas.UsersGetOKUsersItemInboundsItem{{ ID: 3, Label: "RU1-force", Port: 8443, Missing: false, @@ -74,14 +80,17 @@ func TestHandler_UsersGet(t *testing.T) { { name: "empty", params: oas.UsersGetParams{}, - buildUsersMock: func(m *MockuserLister) { + buildUsersMock: func(m *MockusersRepo) { m.EXPECT(). ListPage(gomock.Any(), entity.UserListParams{Limit: 50, Offset: 0}). Return(entity.UserPage{}, nil) }, - buildFleetMock: func(m *MockfleetReader) { + buildFleetMock: func(m *MockfleetService) { m.EXPECT().Fleet(gomock.Any()).Return(&entity.Fleet{}, nil) }, + buildLinksMock: func(m *MocksublinksService) { + m.EXPECT().Links(gomock.Any(), gomock.Any()).Return(map[int64][]entity.SubLink{}, nil) + }, result: &oas.UsersGetOK{ Users: []oas.UsersGetOKUsersItem{}, Total: 0, @@ -92,11 +101,27 @@ func TestHandler_UsersGet(t *testing.T) { { name: "error.list", params: oas.UsersGetParams{}, - buildUsersMock: func(m *MockuserLister) { + buildUsersMock: func(m *MockusersRepo) { m.EXPECT().ListPage(gomock.Any(), entity.UserListParams{Limit: 50, Offset: 0}).Return(entity.UserPage{}, internalErr) }, err: internalErr, }, + { + name: "error.links", + params: oas.UsersGetParams{}, + buildUsersMock: func(m *MockusersRepo) { + m.EXPECT(). + ListPage(gomock.Any(), entity.UserListParams{Limit: 50, Offset: 0}). + Return(entity.UserPage{Users: []entity.User{{ID: 7, Name: "alice", SubID: subID}}, Total: 1}, nil) + }, + buildFleetMock: func(m *MockfleetService) { + m.EXPECT().Fleet(gomock.Any()).Return(&entity.Fleet{}, nil) + }, + buildLinksMock: func(m *MocksublinksService) { + m.EXPECT().Links(gomock.Any(), gomock.Any()).Return(nil, internalErr) + }, + err: internalErr, + }, } t.Parallel() @@ -106,17 +131,22 @@ func TestHandler_UsersGet(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - users := NewMockuserLister(ctrl) + users := NewMockusersRepo(ctrl) if tc.buildUsersMock != nil { tc.buildUsersMock(users) } - fleet := NewMockfleetReader(ctrl) + fleet := NewMockfleetService(ctrl) if tc.buildFleetMock != nil { tc.buildFleetMock(fleet) } - res, err := New(users, fleet, secret, base).UsersGet(context.Background(), tc.params) + links := NewMocksublinksService(ctrl) + if tc.buildLinksMock != nil { + tc.buildLinksMock(links) + } + + res, err := New(users, fleet, links).UsersGet(context.Background(), tc.params) require.ErrorIs(t, err, tc.err) assert.Equal(t, tc.result, res) diff --git a/internal/handlers/web/static/app.css b/internal/handlers/web/static/app.css index 1bafbb3..70e1d4e 100644 --- a/internal/handlers/web/static/app.css +++ b/internal/handlers/web/static/app.css @@ -223,6 +223,12 @@ tr.acting{opacity:.5;} .inb-row .form-control{max-width:150px;} .inb-empty{color:var(--bs-secondary-color);font-size:.85rem;margin:.2rem 0 .6rem;} +/* ===== subscription-links popup (title + copy; the link value is private, not shown) ===== */ +.sublinks{display:flex;flex-direction:column;gap:.55rem;} +.sublink{display:flex;align-items:center;gap:.75rem;padding:.6rem .8rem;border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);background:var(--input-bg);} +.sublink-title{flex:1;min-width:0;font-weight:600;color:#fff;font-size:.9rem;} +.sublink-copy{flex:none;} + /* ===== rule-provider list (compact rows; full edit in modal) ===== */ .editrow{display:flex;gap:.5rem;align-items:center;margin-bottom:.55rem;} .editrow .grow{flex:1;min-width:0;} diff --git a/internal/handlers/web/static/app.js b/internal/handlers/web/static/app.js index 543b777..f493aad 100644 --- a/internal/handlers/web/static/app.js +++ b/internal/handlers/web/static/app.js @@ -24,6 +24,7 @@ const app = createApp({ nodes: [], schema: null, // config UI schema (rule/group/policy/provider catalogs), from the backend uForm: { open: false, id: 0, name: "", description: "", inbounds: [] }, + subLinks: { open: false, name: "", links: [] }, // subscription-links popup (links come from the backend) nodeForm: { open: false, id: 0, name: "", vpnHost: "", panelBaseURL: "", panelBasePath: "", token: "", inbounds: [] }, provForm: { open: false, idx: -1 }, // which provider the edit modal is editing @@ -430,6 +431,10 @@ const app = createApp({ // ---- users -------------------------------------------------------------- openCreateUser() { this.uForm = { open: true, id: 0, name: "", description: "", inbounds: [] }; }, + // openSubLinks shows the subscription-links popup for a user. The list (titles + + // values) is whatever the users API reported under sub.links — nothing is hardcoded + // here, so new link kinds appear automatically. + openSubLinks(u) { this.subLinks = { open: true, name: u.name, links: (u.sub && u.sub.links) || [] }; }, openEdit(u) { this.uForm = { open: true, id: u.id, name: u.name, description: u.description || "", inbounds: (u.inbounds || []).map((i) => i.id) }; }, @@ -480,7 +485,7 @@ const app = createApp({ if (d.ok) this.load("nodes"); }, - closeModals() { this.uForm.open = false; this.nodeForm.open = false; this.inboundFilterOpen = false; }, + closeModals() { this.uForm.open = false; this.nodeForm.open = false; this.inboundFilterOpen = false; this.subLinks.open = false; }, }, mounted() { this.load("users"); diff --git a/internal/handlers/web/static/index.html b/internal/handlers/web/static/index.html index 379c0e0..62941fc 100644 --- a/internal/handlers/web/static/index.html +++ b/internal/handlers/web/static/index.html @@ -77,7 +77,7 @@ <h1 class="page-title">Пользователи</h1> <span v-else class="text-dim small">—</span> </td> <td class="traffic"><span class="up">↑{{ hsize(u.stats.up) }}</span><span class="down">↓{{ hsize(u.stats.down) }}</span></td> - <td><button class="btn btn-sm btn-outline-secondary act" @click="copy(u.sub.url)">Mihomo</button></td> + <td><button class="btn btn-sm btn-outline-secondary act" @click="openSubLinks(u)">Ссылки</button></td> <td class="actions"> <span v-if="actingId===u.id" class="spin me-2"></span> <div class="act-group"> @@ -157,6 +157,27 @@ <h1 class="page-title">Конфиг</h1> <span>Кастомный конфиг — <b>{{ cfgScope.name }}</b></span> <button class="btn btn-sm btn-danger-soft ms-auto" :disabled="busy" @click="deleteCustom()">Удалить</button> </div> + <!-- subscription profile --> + <div class="card mb-3"> + <div class="card-header">Параметры подписки</div> + <div class="card-body"> + <div class="d-flex flex-wrap gap-3"> + <label class="flex-fill"> + <span class="text-dim small d-block mb-1">Profile title</span> + <input class="form-control form-control-sm" v-model="cfg.profileTitle" placeholder="Freedom"> + </label> + <label class="flex-fill"> + <span class="text-dim small d-block mb-1">Имя файла</span> + <input class="form-control form-control-sm" v-model="cfg.filename" placeholder="freedom.yaml"> + </label> + <label style="max-width:200px"> + <span class="text-dim small d-block mb-1">Интервал обновления, ч</span> + <input class="form-control form-control-sm" type="number" min="1" v-model.number="cfg.profileUpdateInterval" placeholder="1"> + </label> + </div> + </div> + </div> + <!-- proxy-groups --> <div class="card mb-3"> <div class="card-header">proxy-groups @@ -250,27 +271,6 @@ <h1 class="page-title">Конфиг</h1> </div> </div> - <!-- subscription profile --> - <div class="card mb-3"> - <div class="card-header">Параметры подписки</div> - <div class="card-body"> - <div class="d-flex flex-wrap gap-3"> - <label class="flex-fill"> - <span class="text-dim small d-block mb-1">Profile title</span> - <input class="form-control form-control-sm" v-model="cfg.profileTitle" placeholder="Freedom"> - </label> - <label class="flex-fill"> - <span class="text-dim small d-block mb-1">Имя файла</span> - <input class="form-control form-control-sm" v-model="cfg.filename" placeholder="freedom.yaml"> - </label> - <label style="max-width:200px"> - <span class="text-dim small d-block mb-1">Интервал обновления, ч</span> - <input class="form-control form-control-sm" type="number" min="1" v-model.number="cfg.profileUpdateInterval" placeholder="1"> - </label> - </div> - </div> - </div> - <div class="card"> <div class="card-header">Прочие настройки (YAML) <span class="sub">без {{ (schema?.generatedKeys||[]).join(' / ') }} — они генерируются</span></div> <div class="card-body"> @@ -375,6 +375,23 @@ <h1 class="page-title">Конфиг</h1> </template> </modal> + <!-- ============ SUBSCRIPTION-LINKS MODAL ============ --> + <!-- The list of links comes entirely from the users API — nothing about which links + exist or their titles is hardcoded here. The value is private and not shown: + title + a copy button only. --> + <modal :open="subLinks.open" :title="'Подписка — ' + subLinks.name" @close="subLinks.open=false"> + <div class="sublinks"> + <div v-for="(l,i) in subLinks.links" :key="i" class="sublink"> + <div class="sublink-title">{{ l.title }}</div> + <button class="btn btn-sm btn-outline-secondary sublink-copy" @click="copy(l.value)">Копировать</button> + </div> + <div v-if="!subLinks.links.length" class="inb-empty">ссылок нет</div> + </div> + <template #footer> + <button class="btn btn-outline-secondary" @click="subLinks.open=false">Закрыть</button> + </template> + </modal> + <!-- ============ NODE MODAL (create / edit) ============ --> <modal :open="nodeForm.open" lg :title="nodeForm.id ? 'Изменить узел' : 'Новый узел'" @close="nodeForm.open=false"> <div class="row g-3"> diff --git a/internal/oas/oas_json_gen.go b/internal/oas/oas_json_gen.go index 8cf380e..8a1494c 100644 --- a/internal/oas/oas_json_gen.go +++ b/internal/oas/oas_json_gen.go @@ -6211,18 +6211,17 @@ func (s *UsersGetOKUsersItemSub) Encode(e *jx.Encoder) { // encodeFields encodes fields. func (s *UsersGetOKUsersItemSub) encodeFields(e *jx.Encoder) { { - e.FieldStart("id") - e.Str(s.ID) - } - { - e.FieldStart("url") - e.Str(s.URL) + e.FieldStart("links") + e.ArrStart() + for _, elem := range s.Links { + elem.Encode(e) + } + e.ArrEnd() } } -var jsonFieldsNameOfUsersGetOKUsersItemSub = [2]string{ - 0: "id", - 1: "url", +var jsonFieldsNameOfUsersGetOKUsersItemSub = [1]string{ + 0: "links", } // Decode decodes UsersGetOKUsersItemSub from json. @@ -6234,36 +6233,143 @@ func (s *UsersGetOKUsersItemSub) Decode(d *jx.Decoder) error { if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "id": + case "links": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Links = make([]UsersGetOKUsersItemSubLinksItem, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem UsersGetOKUsersItemSubLinksItem + if err := elem.Decode(d); err != nil { + return err + } + s.Links = append(s.Links, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"links\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode UsersGetOKUsersItemSub") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfUsersGetOKUsersItemSub) { + name = jsonFieldsNameOfUsersGetOKUsersItemSub[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *UsersGetOKUsersItemSub) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *UsersGetOKUsersItemSub) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *UsersGetOKUsersItemSubLinksItem) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *UsersGetOKUsersItemSubLinksItem) encodeFields(e *jx.Encoder) { + { + e.FieldStart("title") + e.Str(s.Title) + } + { + e.FieldStart("value") + e.Str(s.Value) + } +} + +var jsonFieldsNameOfUsersGetOKUsersItemSubLinksItem = [2]string{ + 0: "title", + 1: "value", +} + +// Decode decodes UsersGetOKUsersItemSubLinksItem from json. +func (s *UsersGetOKUsersItemSubLinksItem) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode UsersGetOKUsersItemSubLinksItem to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "title": requiredBitSet[0] |= 1 << 0 if err := func() error { v, err := d.Str() - s.ID = string(v) + s.Title = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"id\"") + return errors.Wrap(err, "decode field \"title\"") } - case "url": + case "value": requiredBitSet[0] |= 1 << 1 if err := func() error { v, err := d.Str() - s.URL = string(v) + s.Value = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"url\"") + return errors.Wrap(err, "decode field \"value\"") } default: return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode UsersGetOKUsersItemSub") + return errors.Wrap(err, "decode UsersGetOKUsersItemSubLinksItem") } // Validate required fields. var failures []validate.FieldError @@ -6280,8 +6386,8 @@ func (s *UsersGetOKUsersItemSub) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfUsersGetOKUsersItemSub) { - name = jsonFieldsNameOfUsersGetOKUsersItemSub[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfUsersGetOKUsersItemSubLinksItem) { + name = jsonFieldsNameOfUsersGetOKUsersItemSubLinksItem[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -6302,14 +6408,14 @@ func (s *UsersGetOKUsersItemSub) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *UsersGetOKUsersItemSub) MarshalJSON() ([]byte, error) { +func (s *UsersGetOKUsersItemSubLinksItem) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *UsersGetOKUsersItemSub) UnmarshalJSON(data []byte) error { +func (s *UsersGetOKUsersItemSubLinksItem) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } diff --git a/internal/oas/oas_schemas_gen.go b/internal/oas/oas_schemas_gen.go index 43e7a28..726de5e 100644 --- a/internal/oas/oas_schemas_gen.go +++ b/internal/oas/oas_schemas_gen.go @@ -1972,10 +1972,13 @@ type UsersGetOKUsersItem struct { ID int64 `json:"id"` Name string `json:"name"` // Optional free-text note, shown only in the admin UI; omitted when unset. - Description OptString `json:"description"` - Sub UsersGetOKUsersItemSub `json:"sub"` - Inbounds []UsersGetOKUsersItemInboundsItem `json:"inbounds"` - Stats UsersGetOKUsersItemStats `json:"stats"` + Description OptString `json:"description"` + // Subscription presentation: the ordered, copyable links for this user — the raw subscription URL + // plus app deeplinks that embed it. Titles and which links exist are decided by the backend; the UI + // renders the list verbatim and hardcodes neither. + Sub UsersGetOKUsersItemSub `json:"sub"` + Inbounds []UsersGetOKUsersItemInboundsItem `json:"inbounds"` + Stats UsersGetOKUsersItemStats `json:"stats"` } // GetID returns the value of ID. @@ -2110,27 +2113,44 @@ func (s *UsersGetOKUsersItemStats) SetDown(val int64) { s.Down = val } +// Subscription presentation: the ordered, copyable links for this user — the raw subscription URL +// plus app deeplinks that embed it. Titles and which links exist are decided by the backend; the UI +// renders the list verbatim and hardcodes neither. type UsersGetOKUsersItemSub struct { - ID string `json:"id"` - URL string `json:"url"` + Links []UsersGetOKUsersItemSubLinksItem `json:"links"` } -// GetID returns the value of ID. -func (s *UsersGetOKUsersItemSub) GetID() string { - return s.ID +// GetLinks returns the value of Links. +func (s *UsersGetOKUsersItemSub) GetLinks() []UsersGetOKUsersItemSubLinksItem { + return s.Links } -// GetURL returns the value of URL. -func (s *UsersGetOKUsersItemSub) GetURL() string { - return s.URL +// SetLinks sets the value of Links. +func (s *UsersGetOKUsersItemSub) SetLinks(val []UsersGetOKUsersItemSubLinksItem) { + s.Links = val } -// SetID sets the value of ID. -func (s *UsersGetOKUsersItemSub) SetID(val string) { - s.ID = val +type UsersGetOKUsersItemSubLinksItem struct { + Title string `json:"title"` + Value string `json:"value"` } -// SetURL sets the value of URL. -func (s *UsersGetOKUsersItemSub) SetURL(val string) { - s.URL = val +// GetTitle returns the value of Title. +func (s *UsersGetOKUsersItemSubLinksItem) GetTitle() string { + return s.Title +} + +// GetValue returns the value of Value. +func (s *UsersGetOKUsersItemSubLinksItem) GetValue() string { + return s.Value +} + +// SetTitle sets the value of Title. +func (s *UsersGetOKUsersItemSubLinksItem) SetTitle(val string) { + s.Title = val +} + +// SetValue sets the value of Value. +func (s *UsersGetOKUsersItemSubLinksItem) SetValue(val string) { + s.Value = val } diff --git a/internal/oas/oas_validators_gen.go b/internal/oas/oas_validators_gen.go index 18d767d..dcab206 100644 --- a/internal/oas/oas_validators_gen.go +++ b/internal/oas/oas_validators_gen.go @@ -595,6 +595,17 @@ func (s *UsersGetOKUsersItem) Validate() error { } var failures []validate.FieldError + if err := func() error { + if err := s.Sub.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "sub", + Error: err, + }) + } if err := func() error { if s.Inbounds == nil { return errors.New("nil is invalid value") @@ -611,3 +622,26 @@ func (s *UsersGetOKUsersItem) Validate() error { } return nil } + +func (s *UsersGetOKUsersItemSub) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Links == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "links", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} diff --git a/internal/repository/configs/contract.go b/internal/repository/configs/contract.go index 07c19af..b999ff8 100644 --- a/internal/repository/configs/contract.go +++ b/internal/repository/configs/contract.go @@ -6,9 +6,9 @@ import ( "database/sql" ) -// cloner copies one engine's content from a source config to a destination config +// routingRepo copies one engine's content from a source config to a destination config // within the caller's transaction (the engine-specific half of CreateUserConfig). // For mihomo this is routing.Repository.CloneConfig. -type cloner interface { +type routingRepo interface { CloneConfig(ctx context.Context, tx *sql.Tx, srcConfigID, dstConfigID int64) error } diff --git a/internal/repository/configs/contract_mocks.go b/internal/repository/configs/contract_mocks.go index 6c71fd8..5413280 100644 --- a/internal/repository/configs/contract_mocks.go +++ b/internal/repository/configs/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// Mockcloner is a mock of cloner interface. -type Mockcloner struct { +// MockroutingRepo is a mock of routingRepo interface. +type MockroutingRepo struct { ctrl *gomock.Controller - recorder *MockclonerMockRecorder + recorder *MockroutingRepoMockRecorder isgomock struct{} } -// MockclonerMockRecorder is the mock recorder for Mockcloner. -type MockclonerMockRecorder struct { - mock *Mockcloner +// MockroutingRepoMockRecorder is the mock recorder for MockroutingRepo. +type MockroutingRepoMockRecorder struct { + mock *MockroutingRepo } -// NewMockcloner creates a new mock instance. -func NewMockcloner(ctrl *gomock.Controller) *Mockcloner { - mock := &Mockcloner{ctrl: ctrl} - mock.recorder = &MockclonerMockRecorder{mock} +// NewMockroutingRepo creates a new mock instance. +func NewMockroutingRepo(ctrl *gomock.Controller) *MockroutingRepo { + mock := &MockroutingRepo{ctrl: ctrl} + mock.recorder = &MockroutingRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockcloner) EXPECT() *MockclonerMockRecorder { +func (m *MockroutingRepo) EXPECT() *MockroutingRepoMockRecorder { return m.recorder } // CloneConfig mocks base method. -func (m *Mockcloner) CloneConfig(ctx context.Context, tx *sql.Tx, srcConfigID, dstConfigID int64) error { +func (m *MockroutingRepo) CloneConfig(ctx context.Context, tx *sql.Tx, srcConfigID, dstConfigID int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CloneConfig", ctx, tx, srcConfigID, dstConfigID) ret0, _ := ret[0].(error) @@ -50,7 +50,7 @@ func (m *Mockcloner) CloneConfig(ctx context.Context, tx *sql.Tx, srcConfigID, d } // CloneConfig indicates an expected call of CloneConfig. -func (mr *MockclonerMockRecorder) CloneConfig(ctx, tx, srcConfigID, dstConfigID any) *gomock.Call { +func (mr *MockroutingRepoMockRecorder) CloneConfig(ctx, tx, srcConfigID, dstConfigID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloneConfig", reflect.TypeOf((*Mockcloner)(nil).CloneConfig), ctx, tx, srcConfigID, dstConfigID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloneConfig", reflect.TypeOf((*MockroutingRepo)(nil).CloneConfig), ctx, tx, srcConfigID, dstConfigID) } diff --git a/internal/repository/configs/create_user_config.go b/internal/repository/configs/create_user_config.go index 5424dfb..7848376 100644 --- a/internal/repository/configs/create_user_config.go +++ b/internal/repository/configs/create_user_config.go @@ -51,7 +51,7 @@ func (r *Repository) CreateUserConfig(ctx context.Context, userID int64, kind en newID, _ := res.LastInsertId() if hasBase { - if err := r.cloner.CloneConfig(ctx, tx, baseID, newID); err != nil { + if err := r.routing.CloneConfig(ctx, tx, baseID, newID); err != nil { return 0, err } } diff --git a/internal/repository/configs/repository.go b/internal/repository/configs/repository.go index ee9afcd..c7cb6db 100644 --- a/internal/repository/configs/repository.go +++ b/internal/repository/configs/repository.go @@ -1,18 +1,18 @@ // Package configs is the engine-agnostic repository for subscription-config // ownership: the base config and per-user custom configs (the subscription_configs // anchor). It knows nothing about any engine's content tables — cloning a base into -// a new custom config is delegated to an engine-specific cloner. One public method +// a new custom config is delegated to an engine-specific content repo. One public method // per file. package configs import "database/sql" -// Repository accesses the subscription_configs table. The cloner copies a base -// config's engine content into a freshly created custom config. +// Repository accesses the subscription_configs table. routing copies a base config's +// engine content into a freshly created custom config. type Repository struct { - db *sql.DB - cloner cloner + db *sql.DB + routing routingRepo } -// New builds a configs repository over the given database handle and content cloner. -func New(db *sql.DB, c cloner) *Repository { return &Repository{db: db, cloner: c} } +// New builds a configs repository over the given database handle and content repo. +func New(db *sql.DB, routing routingRepo) *Repository { return &Repository{db: db, routing: routing} } diff --git a/internal/service/fleet/contract.go b/internal/service/fleet/contract.go index 8759fd7..3fed352 100644 --- a/internal/service/fleet/contract.go +++ b/internal/service/fleet/contract.go @@ -7,8 +7,8 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// nodeLister lists the node registry (the nodes repository satisfies it). -type nodeLister interface { +// nodesRepo lists the node registry (the nodes repository satisfies it). +type nodesRepo interface { List(ctx context.Context) ([]entity.Node, error) } diff --git a/internal/service/fleet/contract_mocks.go b/internal/service/fleet/contract_mocks.go index deba093..73edc15 100644 --- a/internal/service/fleet/contract_mocks.go +++ b/internal/service/fleet/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MocknodeLister is a mock of nodeLister interface. -type MocknodeLister struct { +// MocknodesRepo is a mock of nodesRepo interface. +type MocknodesRepo struct { ctrl *gomock.Controller - recorder *MocknodeListerMockRecorder + recorder *MocknodesRepoMockRecorder isgomock struct{} } -// MocknodeListerMockRecorder is the mock recorder for MocknodeLister. -type MocknodeListerMockRecorder struct { - mock *MocknodeLister +// MocknodesRepoMockRecorder is the mock recorder for MocknodesRepo. +type MocknodesRepoMockRecorder struct { + mock *MocknodesRepo } -// NewMocknodeLister creates a new mock instance. -func NewMocknodeLister(ctrl *gomock.Controller) *MocknodeLister { - mock := &MocknodeLister{ctrl: ctrl} - mock.recorder = &MocknodeListerMockRecorder{mock} +// NewMocknodesRepo creates a new mock instance. +func NewMocknodesRepo(ctrl *gomock.Controller) *MocknodesRepo { + mock := &MocknodesRepo{ctrl: ctrl} + mock.recorder = &MocknodesRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeLister) EXPECT() *MocknodeListerMockRecorder { +func (m *MocknodesRepo) EXPECT() *MocknodesRepoMockRecorder { return m.recorder } // List mocks base method. -func (m *MocknodeLister) List(ctx context.Context) ([]entity.Node, error) { +func (m *MocknodesRepo) List(ctx context.Context) ([]entity.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ctx) ret0, _ := ret[0].([]entity.Node) @@ -51,9 +51,9 @@ func (m *MocknodeLister) List(ctx context.Context) ([]entity.Node, error) { } // List indicates an expected call of List. -func (mr *MocknodeListerMockRecorder) List(ctx any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) List(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodeLister)(nil).List), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodesRepo)(nil).List), ctx) } // MockpanelClient is a mock of panelClient interface. diff --git a/internal/service/fleet/fleet.go b/internal/service/fleet/fleet.go index d536cec..7432be5 100644 --- a/internal/service/fleet/fleet.go +++ b/internal/service/fleet/fleet.go @@ -16,11 +16,11 @@ import ( // Service fetches every node's inbounds and assembles the fleet. type Service struct { client panelClient - nodes nodeLister + nodes nodesRepo } // New builds the fleet service. -func New(client panelClient, nodes nodeLister) *Service { +func New(client panelClient, nodes nodesRepo) *Service { return &Service{client: client, nodes: nodes} } diff --git a/internal/service/nodes/contract.go b/internal/service/nodes/contract.go index 426cf8b..93d40e1 100644 --- a/internal/service/nodes/contract.go +++ b/internal/service/nodes/contract.go @@ -7,10 +7,10 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// nodeRepo is the nodes repository subset the service needs. Referential integrity (an +// nodesRepo is the nodes repository subset the service needs. Referential integrity (an // inbound still referenced on update/delete) is enforced by the database FK and returned // as entity.ErrInboundReferenced — the service does not pre-check it. -type nodeRepo interface { +type nodesRepo interface { Create(ctx context.Context, n entity.Node) (int64, error) Update(ctx context.Context, id int64, n entity.Node, setToken bool) error Delete(ctx context.Context, id int64) error diff --git a/internal/service/nodes/contract_mocks.go b/internal/service/nodes/contract_mocks.go index b48705f..3255bcf 100644 --- a/internal/service/nodes/contract_mocks.go +++ b/internal/service/nodes/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MocknodeRepo is a mock of nodeRepo interface. -type MocknodeRepo struct { +// MocknodesRepo is a mock of nodesRepo interface. +type MocknodesRepo struct { ctrl *gomock.Controller - recorder *MocknodeRepoMockRecorder + recorder *MocknodesRepoMockRecorder isgomock struct{} } -// MocknodeRepoMockRecorder is the mock recorder for MocknodeRepo. -type MocknodeRepoMockRecorder struct { - mock *MocknodeRepo +// MocknodesRepoMockRecorder is the mock recorder for MocknodesRepo. +type MocknodesRepoMockRecorder struct { + mock *MocknodesRepo } -// NewMocknodeRepo creates a new mock instance. -func NewMocknodeRepo(ctrl *gomock.Controller) *MocknodeRepo { - mock := &MocknodeRepo{ctrl: ctrl} - mock.recorder = &MocknodeRepoMockRecorder{mock} +// NewMocknodesRepo creates a new mock instance. +func NewMocknodesRepo(ctrl *gomock.Controller) *MocknodesRepo { + mock := &MocknodesRepo{ctrl: ctrl} + mock.recorder = &MocknodesRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeRepo) EXPECT() *MocknodeRepoMockRecorder { +func (m *MocknodesRepo) EXPECT() *MocknodesRepoMockRecorder { return m.recorder } // Create mocks base method. -func (m *MocknodeRepo) Create(ctx context.Context, n entity.Node) (int64, error) { +func (m *MocknodesRepo) Create(ctx context.Context, n entity.Node) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ctx, n) ret0, _ := ret[0].(int64) @@ -51,13 +51,13 @@ func (m *MocknodeRepo) Create(ctx context.Context, n entity.Node) (int64, error) } // Create indicates an expected call of Create. -func (mr *MocknodeRepoMockRecorder) Create(ctx, n any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) Create(ctx, n any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MocknodeRepo)(nil).Create), ctx, n) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MocknodesRepo)(nil).Create), ctx, n) } // Delete mocks base method. -func (m *MocknodeRepo) Delete(ctx context.Context, id int64) error { +func (m *MocknodesRepo) Delete(ctx context.Context, id int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ctx, id) ret0, _ := ret[0].(error) @@ -65,13 +65,13 @@ func (m *MocknodeRepo) Delete(ctx context.Context, id int64) error { } // Delete indicates an expected call of Delete. -func (mr *MocknodeRepoMockRecorder) Delete(ctx, id any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) Delete(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MocknodeRepo)(nil).Delete), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MocknodesRepo)(nil).Delete), ctx, id) } // Update mocks base method. -func (m *MocknodeRepo) Update(ctx context.Context, id int64, n entity.Node, setToken bool) error { +func (m *MocknodesRepo) Update(ctx context.Context, id int64, n entity.Node, setToken bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", ctx, id, n, setToken) ret0, _ := ret[0].(error) @@ -79,7 +79,7 @@ func (m *MocknodeRepo) Update(ctx context.Context, id int64, n entity.Node, setT } // Update indicates an expected call of Update. -func (mr *MocknodeRepoMockRecorder) Update(ctx, id, n, setToken any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) Update(ctx, id, n, setToken any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MocknodeRepo)(nil).Update), ctx, id, n, setToken) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MocknodesRepo)(nil).Update), ctx, id, n, setToken) } diff --git a/internal/service/nodes/service.go b/internal/service/nodes/service.go index 3572587..3b85542 100644 --- a/internal/service/nodes/service.go +++ b/internal/service/nodes/service.go @@ -15,11 +15,11 @@ import ( // Service owns node operations over the nodes repository. type Service struct { - nodes nodeRepo + nodes nodesRepo } // New builds the service. -func New(nodes nodeRepo) *Service { +func New(nodes nodesRepo) *Service { return &Service{nodes: nodes} } diff --git a/internal/service/nodes/service_test.go b/internal/service/nodes/service_test.go index d66176a..0186669 100644 --- a/internal/service/nodes/service_test.go +++ b/internal/service/nodes/service_test.go @@ -34,7 +34,7 @@ func TestService_Save(t *testing.T) { tt := []struct { name string node entity.Node - buildMock func(m *MocknodeRepo) + buildMock func(m *MocknodesRepo) wantID int64 err error // sentinel/target to ErrorIs; nil => success }{ @@ -44,34 +44,34 @@ func TestService_Save(t *testing.T) { }, { name: "success.create", node: validNode(), wantID: 5, - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Create(gomock.Any(), validNode()).Return(int64(5), nil) }, }, { name: "error.create_conflict", node: validNode(), err: entity.ErrNodeNameTaken, - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Create(gomock.Any(), validNode()).Return(int64(0), entity.ErrNodeNameTaken) }, }, { name: "success.update", node: updateNode(), wantID: 7, - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Update(gomock.Any(), int64(7), updateNode(), false).Return(nil) }, }, { name: "error.update_referenced", node: updateNode(), err: entity.ErrInboundReferenced, // FK refused a dropped inbound — propagated - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Update(gomock.Any(), int64(7), updateNode(), false).Return(entity.ErrInboundReferenced) }, }, { name: "error.update_repo", node: updateNode(), err: targetErr, - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Update(gomock.Any(), int64(7), updateNode(), false).Return(targetErr) }, }, @@ -84,7 +84,7 @@ func TestService_Save(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - m := NewMocknodeRepo(ctrl) + m := NewMocknodesRepo(ctrl) if tc.buildMock != nil { tc.buildMock(m) } @@ -102,33 +102,33 @@ func TestService_Delete(t *testing.T) { tt := []struct { name string - buildMock func(m *MocknodeRepo) + buildMock func(m *MocknodesRepo) err error }{ { name: "success", - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Delete(gomock.Any(), int64(7)).Return(nil) }, }, { name: "error.not_found", err: entity.ErrNodeNotFound, // repo found no row to delete — propagated - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Delete(gomock.Any(), int64(7)).Return(entity.ErrNodeNotFound) }, }, { name: "error.referenced", err: entity.ErrInboundReferenced, // FK refused a referenced inbound — propagated - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Delete(gomock.Any(), int64(7)).Return(entity.ErrInboundReferenced) }, }, { name: "error.repo", err: targetErr, - buildMock: func(m *MocknodeRepo) { + buildMock: func(m *MocknodesRepo) { m.EXPECT().Delete(gomock.Any(), int64(7)).Return(targetErr) }, }, @@ -141,7 +141,7 @@ func TestService_Delete(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - m := NewMocknodeRepo(ctrl) + m := NewMocknodesRepo(ctrl) if tc.buildMock != nil { tc.buildMock(m) } diff --git a/internal/service/provisioning/contract.go b/internal/service/provisioning/contract.go index 6235e92..c9bfdd3 100644 --- a/internal/service/provisioning/contract.go +++ b/internal/service/provisioning/contract.go @@ -7,8 +7,8 @@ import ( "github.com/postlog/subgen/internal/entity" ) -// userRepo is the users repository subset provisioning needs. -type userRepo interface { +// usersRepo is the users repository subset provisioning needs. +type usersRepo interface { Get(ctx context.Context, id int64) (*entity.User, error) Create(ctx context.Context, u *entity.User) error ReplaceConnections(ctx context.Context, userID int64, inboundIDs []int64) error @@ -16,8 +16,8 @@ type userRepo interface { Delete(ctx context.Context, id int64) error } -// nodeRepo lists the node registry. -type nodeRepo interface { +// nodesRepo lists the node registry. +type nodesRepo interface { List(ctx context.Context) ([]entity.Node, error) } diff --git a/internal/service/provisioning/contract_mocks.go b/internal/service/provisioning/contract_mocks.go index 6f3c9ea..497f01c 100644 --- a/internal/service/provisioning/contract_mocks.go +++ b/internal/service/provisioning/contract_mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockuserRepo is a mock of userRepo interface. -type MockuserRepo struct { +// MockusersRepo is a mock of usersRepo interface. +type MockusersRepo struct { ctrl *gomock.Controller - recorder *MockuserRepoMockRecorder + recorder *MockusersRepoMockRecorder isgomock struct{} } -// MockuserRepoMockRecorder is the mock recorder for MockuserRepo. -type MockuserRepoMockRecorder struct { - mock *MockuserRepo +// MockusersRepoMockRecorder is the mock recorder for MockusersRepo. +type MockusersRepoMockRecorder struct { + mock *MockusersRepo } -// NewMockuserRepo creates a new mock instance. -func NewMockuserRepo(ctrl *gomock.Controller) *MockuserRepo { - mock := &MockuserRepo{ctrl: ctrl} - mock.recorder = &MockuserRepoMockRecorder{mock} +// NewMockusersRepo creates a new mock instance. +func NewMockusersRepo(ctrl *gomock.Controller) *MockusersRepo { + mock := &MockusersRepo{ctrl: ctrl} + mock.recorder = &MockusersRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockuserRepo) EXPECT() *MockuserRepoMockRecorder { +func (m *MockusersRepo) EXPECT() *MockusersRepoMockRecorder { return m.recorder } // Create mocks base method. -func (m *MockuserRepo) Create(ctx context.Context, u *entity.User) error { +func (m *MockusersRepo) Create(ctx context.Context, u *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ctx, u) ret0, _ := ret[0].(error) @@ -50,13 +50,13 @@ func (m *MockuserRepo) Create(ctx context.Context, u *entity.User) error { } // Create indicates an expected call of Create. -func (mr *MockuserRepoMockRecorder) Create(ctx, u any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) Create(ctx, u any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockuserRepo)(nil).Create), ctx, u) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockusersRepo)(nil).Create), ctx, u) } // Delete mocks base method. -func (m *MockuserRepo) Delete(ctx context.Context, id int64) error { +func (m *MockusersRepo) Delete(ctx context.Context, id int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ctx, id) ret0, _ := ret[0].(error) @@ -64,13 +64,13 @@ func (m *MockuserRepo) Delete(ctx context.Context, id int64) error { } // Delete indicates an expected call of Delete. -func (mr *MockuserRepoMockRecorder) Delete(ctx, id any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) Delete(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockuserRepo)(nil).Delete), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockusersRepo)(nil).Delete), ctx, id) } // Get mocks base method. -func (m *MockuserRepo) Get(ctx context.Context, id int64) (*entity.User, error) { +func (m *MockusersRepo) Get(ctx context.Context, id int64) (*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, id) ret0, _ := ret[0].(*entity.User) @@ -79,13 +79,13 @@ func (m *MockuserRepo) Get(ctx context.Context, id int64) (*entity.User, error) } // Get indicates an expected call of Get. -func (mr *MockuserRepoMockRecorder) Get(ctx, id any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) Get(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockuserRepo)(nil).Get), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockusersRepo)(nil).Get), ctx, id) } // ReplaceConnections mocks base method. -func (m *MockuserRepo) ReplaceConnections(ctx context.Context, userID int64, inboundIDs []int64) error { +func (m *MockusersRepo) ReplaceConnections(ctx context.Context, userID int64, inboundIDs []int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ReplaceConnections", ctx, userID, inboundIDs) ret0, _ := ret[0].(error) @@ -93,13 +93,13 @@ func (m *MockuserRepo) ReplaceConnections(ctx context.Context, userID int64, inb } // ReplaceConnections indicates an expected call of ReplaceConnections. -func (mr *MockuserRepoMockRecorder) ReplaceConnections(ctx, userID, inboundIDs any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) ReplaceConnections(ctx, userID, inboundIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceConnections", reflect.TypeOf((*MockuserRepo)(nil).ReplaceConnections), ctx, userID, inboundIDs) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceConnections", reflect.TypeOf((*MockusersRepo)(nil).ReplaceConnections), ctx, userID, inboundIDs) } // SetDescription mocks base method. -func (m *MockuserRepo) SetDescription(ctx context.Context, userID int64, description *string) error { +func (m *MockusersRepo) SetDescription(ctx context.Context, userID int64, description *string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDescription", ctx, userID, description) ret0, _ := ret[0].(error) @@ -107,37 +107,37 @@ func (m *MockuserRepo) SetDescription(ctx context.Context, userID int64, descrip } // SetDescription indicates an expected call of SetDescription. -func (mr *MockuserRepoMockRecorder) SetDescription(ctx, userID, description any) *gomock.Call { +func (mr *MockusersRepoMockRecorder) SetDescription(ctx, userID, description any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDescription", reflect.TypeOf((*MockuserRepo)(nil).SetDescription), ctx, userID, description) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDescription", reflect.TypeOf((*MockusersRepo)(nil).SetDescription), ctx, userID, description) } -// MocknodeRepo is a mock of nodeRepo interface. -type MocknodeRepo struct { +// MocknodesRepo is a mock of nodesRepo interface. +type MocknodesRepo struct { ctrl *gomock.Controller - recorder *MocknodeRepoMockRecorder + recorder *MocknodesRepoMockRecorder isgomock struct{} } -// MocknodeRepoMockRecorder is the mock recorder for MocknodeRepo. -type MocknodeRepoMockRecorder struct { - mock *MocknodeRepo +// MocknodesRepoMockRecorder is the mock recorder for MocknodesRepo. +type MocknodesRepoMockRecorder struct { + mock *MocknodesRepo } -// NewMocknodeRepo creates a new mock instance. -func NewMocknodeRepo(ctrl *gomock.Controller) *MocknodeRepo { - mock := &MocknodeRepo{ctrl: ctrl} - mock.recorder = &MocknodeRepoMockRecorder{mock} +// NewMocknodesRepo creates a new mock instance. +func NewMocknodesRepo(ctrl *gomock.Controller) *MocknodesRepo { + mock := &MocknodesRepo{ctrl: ctrl} + mock.recorder = &MocknodesRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocknodeRepo) EXPECT() *MocknodeRepoMockRecorder { +func (m *MocknodesRepo) EXPECT() *MocknodesRepoMockRecorder { return m.recorder } // List mocks base method. -func (m *MocknodeRepo) List(ctx context.Context) ([]entity.Node, error) { +func (m *MocknodesRepo) List(ctx context.Context) ([]entity.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ctx) ret0, _ := ret[0].([]entity.Node) @@ -146,9 +146,9 @@ func (m *MocknodeRepo) List(ctx context.Context) ([]entity.Node, error) { } // List indicates an expected call of List. -func (mr *MocknodeRepoMockRecorder) List(ctx any) *gomock.Call { +func (mr *MocknodesRepoMockRecorder) List(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodeRepo)(nil).List), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MocknodesRepo)(nil).List), ctx) } // MockpanelClient is a mock of panelClient interface. diff --git a/internal/service/provisioning/service.go b/internal/service/provisioning/service.go index 4ad9b29..12871fe 100644 --- a/internal/service/provisioning/service.go +++ b/internal/service/provisioning/service.go @@ -27,8 +27,8 @@ import ( // Service provisions users into 3x-ui from the store. type Service struct { - users userRepo - nodes nodeRepo + users usersRepo + nodes nodesRepo client panelClient // Random-id sources, injected so tests can make them deterministic (defaulted in @@ -38,7 +38,7 @@ type Service struct { } // New builds the provisioning service from its dependencies. -func New(users userRepo, nodes nodeRepo, client panelClient) *Service { +func New(users usersRepo, nodes nodesRepo, client panelClient) *Service { return &Service{ users: users, nodes: nodes, client: client, genID: randID, diff --git a/internal/service/provisioning/service_test.go b/internal/service/provisioning/service_test.go index ce4268a..4a649eb 100644 --- a/internal/service/provisioning/service_test.go +++ b/internal/service/provisioning/service_test.go @@ -42,15 +42,15 @@ func n1Target() entity.PanelTarget { // mocks bundles the service's dependency mocks for a test case. type mocks struct { - users *MockuserRepo - nodes *MocknodeRepo + users *MockusersRepo + nodes *MocknodesRepo client *MockpanelClient } func newMocks(ctrl *gomock.Controller) *mocks { return &mocks{ - users: NewMockuserRepo(ctrl), - nodes: NewMocknodeRepo(ctrl), + users: NewMockusersRepo(ctrl), + nodes: NewMocknodesRepo(ctrl), client: NewMockpanelClient(ctrl), } } diff --git a/internal/service/sublinks/contract.go b/internal/service/sublinks/contract.go new file mode 100644 index 0000000..b57dc4d --- /dev/null +++ b/internal/service/sublinks/contract.go @@ -0,0 +1,24 @@ +//go:generate go tool mockgen -source=contract.go -destination contract_mocks.go -package sublinks +package sublinks + +import ( + "context" + + "github.com/postlog/subgen/internal/entity" + "github.com/postlog/subgen/internal/mihomo" +) + +// configsRepo resolves which config a user effectively gets for an engine — a custom +// config when present, else the base — so the deeplink name can carry that config's +// profile title. +type configsRepo interface { + BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) + UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) + UserConfigUserIDs(ctx context.Context, kind entity.ConfigKind) ([]int64, error) +} + +// routingRepo reads a config's subscription-profile knobs; only the title is used here +// (the name an app deeplink labels the imported profile with). +type routingRepo interface { + Profile(ctx context.Context, configID int64) (mihomo.Profile, error) +} diff --git a/internal/service/sublinks/contract_mocks.go b/internal/service/sublinks/contract_mocks.go new file mode 100644 index 0000000..353284e --- /dev/null +++ b/internal/service/sublinks/contract_mocks.go @@ -0,0 +1,129 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: contract.go +// +// Generated by this command: +// +// mockgen -source=contract.go -destination contract_mocks.go -package sublinks +// + +// Package sublinks is a generated GoMock package. +package sublinks + +import ( + context "context" + reflect "reflect" + + entity "github.com/postlog/subgen/internal/entity" + mihomo "github.com/postlog/subgen/internal/mihomo" + gomock "go.uber.org/mock/gomock" +) + +// MockconfigsRepo is a mock of configsRepo interface. +type MockconfigsRepo struct { + ctrl *gomock.Controller + recorder *MockconfigsRepoMockRecorder + isgomock struct{} +} + +// MockconfigsRepoMockRecorder is the mock recorder for MockconfigsRepo. +type MockconfigsRepoMockRecorder struct { + mock *MockconfigsRepo +} + +// NewMockconfigsRepo creates a new mock instance. +func NewMockconfigsRepo(ctrl *gomock.Controller) *MockconfigsRepo { + mock := &MockconfigsRepo{ctrl: ctrl} + mock.recorder = &MockconfigsRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockconfigsRepo) EXPECT() *MockconfigsRepoMockRecorder { + return m.recorder +} + +// BaseConfigID mocks base method. +func (m *MockconfigsRepo) BaseConfigID(ctx context.Context, kind entity.ConfigKind) (int64, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseConfigID", ctx, kind) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// BaseConfigID indicates an expected call of BaseConfigID. +func (mr *MockconfigsRepoMockRecorder) BaseConfigID(ctx, kind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).BaseConfigID), ctx, kind) +} + +// UserConfigID mocks base method. +func (m *MockconfigsRepo) UserConfigID(ctx context.Context, userID int64, kind entity.ConfigKind) (int64, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserConfigID", ctx, userID, kind) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UserConfigID indicates an expected call of UserConfigID. +func (mr *MockconfigsRepoMockRecorder) UserConfigID(ctx, userID, kind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigID", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigID), ctx, userID, kind) +} + +// UserConfigUserIDs mocks base method. +func (m *MockconfigsRepo) UserConfigUserIDs(ctx context.Context, kind entity.ConfigKind) ([]int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserConfigUserIDs", ctx, kind) + ret0, _ := ret[0].([]int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UserConfigUserIDs indicates an expected call of UserConfigUserIDs. +func (mr *MockconfigsRepoMockRecorder) UserConfigUserIDs(ctx, kind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserConfigUserIDs", reflect.TypeOf((*MockconfigsRepo)(nil).UserConfigUserIDs), ctx, kind) +} + +// MockroutingRepo is a mock of routingRepo interface. +type MockroutingRepo struct { + ctrl *gomock.Controller + recorder *MockroutingRepoMockRecorder + isgomock struct{} +} + +// MockroutingRepoMockRecorder is the mock recorder for MockroutingRepo. +type MockroutingRepoMockRecorder struct { + mock *MockroutingRepo +} + +// NewMockroutingRepo creates a new mock instance. +func NewMockroutingRepo(ctrl *gomock.Controller) *MockroutingRepo { + mock := &MockroutingRepo{ctrl: ctrl} + mock.recorder = &MockroutingRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockroutingRepo) EXPECT() *MockroutingRepoMockRecorder { + return m.recorder +} + +// Profile mocks base method. +func (m *MockroutingRepo) Profile(ctx context.Context, configID int64) (mihomo.Profile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Profile", ctx, configID) + ret0, _ := ret[0].(mihomo.Profile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Profile indicates an expected call of Profile. +func (mr *MockroutingRepoMockRecorder) Profile(ctx, configID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Profile", reflect.TypeOf((*MockroutingRepo)(nil).Profile), ctx, configID) +} diff --git a/internal/service/sublinks/sublinks.go b/internal/service/sublinks/sublinks.go new file mode 100644 index 0000000..dea2d19 --- /dev/null +++ b/internal/service/sublinks/sublinks.go @@ -0,0 +1,214 @@ +// Package sublinks builds the per-user list of copyable subscription links shown in the +// admin UI — the raw subscription URL for each engine plus the app deeplinks that embed +// it (clashmi today). The catalog of links lives here, so the admin SPA renders whatever +// the backend declares and hardcodes neither titles nor link formats; adding an engine +// or a deeplink is a one-line catalog change with no frontend edit. +package sublinks + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/postlog/subgen/internal/entity" + "github.com/postlog/subgen/internal/token" +) + +// linkSpec is one entry in the subscription-link catalog: the engine it targets, the +// display title and how to turn the user's resolved sub URL (+ the config's profile +// title) into the copyable value. Keeping the deeplink shape in build() — not as a magic +// string scattered across the code — confines escaping/format to one typed place. +type linkSpec struct { + title string + kind entity.ConfigKind + build func(subURL, profileTitle string) string +} + +// catalog is the ordered set of links offered for every user. Extend it to add an engine +// or an app deeplink; the admin UI needs no change. +var catalog = []linkSpec{ + {title: "Mihomo", kind: entity.ConfigKindMihomo, build: rawURL}, + {title: "Clashmi", kind: entity.ConfigKindMihomo, build: clashmiDeeplink}, +} + +// rawURL is the identity build: the copyable value is the subscription URL itself. +func rawURL(subURL, _ string) string { return subURL } + +// clashmiDeeplink wraps a mihomo subscription URL in clashmi's install-config deeplink. +// name is the config's profile title — what the app labels the imported profile with. +func clashmiDeeplink(subURL, profileTitle string) string { + return "clashmi://install-config?url=" + url.QueryEscape(subURL) + + "&name=" + url.QueryEscape(profileTitle) + + "&overwrite=false" +} + +// Service builds subscription links. It holds the HMAC secret and public base used to +// mint per-user sub URLs and resolves each user's effective profile title via the config +// repositories. +type Service struct { + secret string + base string + configs configsRepo + profiles routingRepo +} + +// New builds the service. base is the public base URL (trailing slash trimmed); secret is +// the HMAC secret behind the subscription token. +func New(secret, base string, configs configsRepo, profiles routingRepo) *Service { + return &Service{ + secret: secret, + base: strings.TrimRight(base, "/"), + configs: configs, + profiles: profiles, + } +} + +// Links builds the ordered copyable links for each user, keyed by user id. For every +// catalog entry it composes the user's sub URL (base + /sub/<kind>/<token>) and, for +// deeplinks, the effective profile title of that engine's config. Titles are resolved +// per engine — the base once, customs only for users that have one — so a page of users +// is a handful of reads, not one per user. +func (s *Service) Links(ctx context.Context, users []entity.User) (map[int64][]entity.SubLink, error) { + if len(users) == 0 { + return map[int64][]entity.SubLink{}, nil + } + + titles, err := s.titlesByUser(ctx, users) + if err != nil { + return nil, err + } + + out := make(map[int64][]entity.SubLink, len(users)) + + for i := range users { + u := &users[i] + + links := make([]entity.SubLink, 0, len(catalog)) + for _, spec := range catalog { + subURL := s.base + "/sub/" + string(spec.kind) + "/" + token.Make(s.secret, u.SubID) + links = append(links, entity.SubLink{ + Title: spec.title, + Value: spec.build(subURL, titles[u.ID][spec.kind]), + }) + } + + out[u.ID] = links + } + + return out, nil +} + +// titlesByUser resolves each user's effective profile title for every engine the catalog +// references: the base title (read once per engine) unless the user has a custom config +// for that engine, whose title is read individually. Returns userID → kind → title. +func (s *Service) titlesByUser(ctx context.Context, users []entity.User) (map[int64]map[entity.ConfigKind]string, error) { + out := make(map[int64]map[entity.ConfigKind]string, len(users)) + for i := range users { + out[users[i].ID] = make(map[entity.ConfigKind]string) + } + + for _, kind := range catalogKinds { + baseTitle, err := s.baseTitle(ctx, kind) + if err != nil { + return nil, err + } + + custom, err := s.customUsers(ctx, kind) + if err != nil { + return nil, err + } + + for i := range users { + u := &users[i] + + title := baseTitle + + if _, ok := custom[u.ID]; ok { + t, err := s.userTitle(ctx, u.ID, kind) + if err != nil { + return nil, err + } + + title = t + } + + out[u.ID][kind] = title + } + } + + return out, nil +} + +// customUsers is the set of user ids that have a custom config for the engine. +func (s *Service) customUsers(ctx context.Context, kind entity.ConfigKind) (map[int64]struct{}, error) { + ids, err := s.configs.UserConfigUserIDs(ctx, kind) + if err != nil { + return nil, fmt.Errorf("configs.UserConfigUserIDs: %w", err) + } + + set := make(map[int64]struct{}, len(ids)) + for _, id := range ids { + set[id] = struct{}{} + } + + return set, nil +} + +// baseTitle reads the engine's base-config profile title, or "" if there is no base yet. +func (s *Service) baseTitle(ctx context.Context, kind entity.ConfigKind) (string, error) { + id, ok, err := s.configs.BaseConfigID(ctx, kind) + if err != nil { + return "", fmt.Errorf("configs.BaseConfigID: %w", err) + } + + if !ok { + return "", nil + } + + return s.title(ctx, id) +} + +// userTitle reads a user's custom-config profile title for the engine, or "" if none. +func (s *Service) userTitle(ctx context.Context, userID int64, kind entity.ConfigKind) (string, error) { + id, ok, err := s.configs.UserConfigID(ctx, userID, kind) + if err != nil { + return "", fmt.Errorf("configs.UserConfigID: %w", err) + } + + if !ok { + return "", nil + } + + return s.title(ctx, id) +} + +// title reads a config's profile title. A config with no profile row yields "". +func (s *Service) title(ctx context.Context, configID int64) (string, error) { + p, err := s.profiles.Profile(ctx, configID) + if err != nil { + return "", fmt.Errorf("profiles.Profile: %w", err) + } + + return p.Title, nil +} + +// catalogKinds is the distinct engine kinds referenced by the catalog, in first-seen +// order. Computed once at package init — the catalog is immutable, so there is no reason +// to recompute it per request. +var catalogKinds = func() []entity.ConfigKind { + seen := make(map[entity.ConfigKind]struct{}, len(catalog)) + + var out []entity.ConfigKind + + for _, spec := range catalog { + if _, ok := seen[spec.kind]; ok { + continue + } + + seen[spec.kind] = struct{}{} + out = append(out, spec.kind) + } + + return out +}() diff --git a/internal/service/sublinks/sublinks_test.go b/internal/service/sublinks/sublinks_test.go new file mode 100644 index 0000000..d307017 --- /dev/null +++ b/internal/service/sublinks/sublinks_test.go @@ -0,0 +1,137 @@ +package sublinks + +import ( + "context" + "errors" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/postlog/subgen/internal/entity" + "github.com/postlog/subgen/internal/mihomo" + "github.com/postlog/subgen/internal/token" +) + +func TestService_Links(t *testing.T) { + const ( + secret = "secret" + base = "http://base/" // trailing slash trimmed by New + ) + + targetErr := errors.New("db down") + + // rawURL/clashmi rebuild the expected values from the same primitives the service + // uses, so a case asserts the full assembly (token, path, escaping, name) end to end. + subURL := func(subID string) string { + return "http://base/sub/mihomo/" + token.Make(secret, subID) + } + clashmi := func(subID, name string) string { + return "clashmi://install-config?url=" + url.QueryEscape(subURL(subID)) + + "&name=" + url.QueryEscape(name) + "&overwrite=false" + } + + tt := []struct { + name string + users []entity.User + buildMock func(m *MockconfigsRepo, p *MockroutingRepo) + result map[int64][]entity.SubLink + err error + }{ + { + name: "empty", + users: nil, + result: map[int64][]entity.SubLink{}, + }, + { + name: "success.base_only", + users: []entity.User{{ID: 7, SubID: "s7"}}, + buildMock: func(m *MockconfigsRepo, p *MockroutingRepo) { + m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(100), true, nil) + p.EXPECT().Profile(gomock.Any(), int64(100)).Return(mihomo.Profile{Title: "Freedom"}, nil) + m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return(nil, nil) + }, + result: map[int64][]entity.SubLink{ + 7: { + {Title: "Mihomo", Value: subURL("s7")}, + {Title: "Clashmi", Value: clashmi("s7", "Freedom")}, + }, + }, + }, + { + name: "success.custom_overrides_title", + users: []entity.User{{ID: 7, SubID: "s7"}, {ID: 8, SubID: "s8"}}, + buildMock: func(m *MockconfigsRepo, p *MockroutingRepo) { + m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(100), true, nil) + p.EXPECT().Profile(gomock.Any(), int64(100)).Return(mihomo.Profile{Title: "Freedom"}, nil) + m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return([]int64{7}, nil) + m.EXPECT().UserConfigID(gomock.Any(), int64(7), entity.ConfigKindMihomo).Return(int64(200), true, nil) + p.EXPECT().Profile(gomock.Any(), int64(200)).Return(mihomo.Profile{Title: "Freedom Pro"}, nil) + }, + result: map[int64][]entity.SubLink{ + 7: { + {Title: "Mihomo", Value: subURL("s7")}, + {Title: "Clashmi", Value: clashmi("s7", "Freedom Pro")}, + }, + 8: { + {Title: "Mihomo", Value: subURL("s8")}, + {Title: "Clashmi", Value: clashmi("s8", "Freedom")}, + }, + }, + }, + { + name: "success.no_base_config", + users: []entity.User{{ID: 7, SubID: "s7"}}, + buildMock: func(m *MockconfigsRepo, p *MockroutingRepo) { + m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(0), false, nil) + m.EXPECT().UserConfigUserIDs(gomock.Any(), entity.ConfigKindMihomo).Return(nil, nil) + }, + result: map[int64][]entity.SubLink{ + 7: { + {Title: "Mihomo", Value: subURL("s7")}, + {Title: "Clashmi", Value: clashmi("s7", "")}, + }, + }, + }, + { + name: "error.base_config", + users: []entity.User{{ID: 7, SubID: "s7"}}, + buildMock: func(m *MockconfigsRepo, _ *MockroutingRepo) { + m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(0), false, targetErr) + }, + err: targetErr, + }, + { + name: "error.profile", + users: []entity.User{{ID: 7, SubID: "s7"}}, + buildMock: func(m *MockconfigsRepo, p *MockroutingRepo) { + m.EXPECT().BaseConfigID(gomock.Any(), entity.ConfigKindMihomo).Return(int64(100), true, nil) + p.EXPECT().Profile(gomock.Any(), int64(100)).Return(mihomo.Profile{}, targetErr) + }, + err: targetErr, + }, + } + + t.Parallel() + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + cfgs := NewMockconfigsRepo(ctrl) + + profiles := NewMockroutingRepo(ctrl) + if tc.buildMock != nil { + tc.buildMock(cfgs, profiles) + } + + res, err := New(secret, base, cfgs, profiles).Links(context.Background(), tc.users) + + require.ErrorIs(t, err, tc.err) + assert.Equal(t, tc.result, res) + }) + } +} diff --git a/openapi/users_get.yaml b/openapi/users_get.yaml index 558e242..bb39d4d 100644 --- a/openapi/users_get.yaml +++ b/openapi/users_get.yaml @@ -51,12 +51,23 @@ get: description: Optional free-text note, shown only in the admin UI; omitted when unset. sub: type: object - required: [id, url] + description: >- + Subscription presentation: the ordered, copyable links for this + user — the raw subscription URL plus app deeplinks that embed it. + Titles and which links exist are decided by the backend; the UI + renders the list verbatim and hardcodes neither. + required: [links] properties: - id: - type: string - url: - type: string + links: + type: array + items: + type: object + required: [title, value] + properties: + title: + type: string + value: + type: string inbounds: type: array items: