From c8b9d08c0ba956f67800f8e6b486f97cb227cfb1 Mon Sep 17 00:00:00 2001 From: byrsapty Date: Thu, 27 Aug 2026 21:30:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=91=D1=96=D0=BB=D1=96=D0=BD=D0=B3:=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D0=B8=D1=82=20=D1=87=D0=B8=D1=82=D0=B0=D0=B2=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=96=20=D1=96=D0=BC=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA=20usage=5Fnow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Агент свідомо перейменував вихідні колонки функції на devices_used, maps_used, agents_used, users_used — щоб `devices` поруч із таблицею inv.devices не давало неоднозначності. Запит у Go лишився на старих іменах. Код збирався, локальні тести були зелені (пропускались без NETPULSE_TEST_DSN), а перше ж звернення до бази падало з «column "devices" does not exist». Сторінка білінгу не показала б нічого. Знайдено пісочницею: установка зі свіжого клону, 63 міграції на чистій базі, потім тести з живою БД. Це той самий клас, що ловився весь день — зелена перевірка доводить рівно те, що перевіряє, а тест, який мовчки пропускається, не перевіряє нічого. Заодно записано в шапці api_test.go умову, якої той набір вимагає: база має бути порожньою. Користувачі в продукті глобальні, і на вже поставленій системі seedUser натрапляє на власника admin. --- server/internal/httpapi/api_test.go | 18 ++++++++++++++++++ server/internal/store/billing.go | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/server/internal/httpapi/api_test.go b/server/internal/httpapi/api_test.go index 3181a01..2428f51 100644 --- a/server/internal/httpapi/api_test.go +++ b/server/internal/httpapi/api_test.go @@ -1,6 +1,24 @@ // Наскрізний тест API: справжня БД, справжній HTTP, справжній WebSocket. // // NETPULSE_TEST_DSN="postgres://netpulse:netpulse@localhost/netpulse_it" go test ./... +// +// БАЗА МАЄ БУТИ ПОРОЖНЬОЮ — лише зі схемою, без даних. +// +// Кабінети тут унікальні (slug із наносекундами), а от КОРИСТУВАЧІ в +// цьому продукті глобальні: одна людина може працювати в кількох +// кабінетах, тому `core.users.username` унікальний наскрізно, і +// CreateUser при зіткненні свідомо підхоплює наявного, НЕ переписуючи +// пароль (users.go, і це правильно: інакше адмін одного кабінету міняв +// би пароль людини в чужому). +// +// Наслідок для тестів: на базі, де вже стоїть робоча інсталяція, +// seedUser("admin@example.com") натрапляє на власника з ім'ям `admin`, +// пароль лишається чужим, вхід не вдається — і тест падає на 401 у +// зовсім іншому місці. Це не поломка продукту, а невиконана умова. +// +// Так знайшлось: набір прогнали проти пісочниці установника, тобто +// проти ВЖЕ поставленої системи. CI і `./netpulse sandbox once` дають +// чисту базу, тому там усе зелене. package httpapi_test import ( diff --git a/server/internal/store/billing.go b/server/internal/store/billing.go index 98d9688..013ab1b 100644 --- a/server/internal/store/billing.go +++ b/server/internal/store/billing.go @@ -298,7 +298,14 @@ func (s *Store) TenantUsage(ctx context.Context, tenantID string) (Usage, error) var u Usage err := s.InTenantTx(ctx, tenantID, func(tx pgx.Tx) error { return tx.QueryRow(ctx, - `SELECT devices, maps, agents, users FROM bill.usage_now($1)`, tenantID). + // Суфікс `_used` — не косметика: функція повертає колонки з + // цими іменами саме тому, що `devices` поруч із таблицею + // inv.devices дає неоднозначність (пояснення в 0069). Тут + // імена спершу лишились старими, і це знайшлось лише в + // пісочниці: код збирався, а перший же запит до бази падав + // з «column "devices" does not exist». + `SELECT devices_used, maps_used, agents_used, users_used + FROM bill.usage_now($1)`, tenantID). Scan(&u.Devices, &u.Maps, &u.Agents, &u.Users) }) return u, err