72de690894
Chatbot functionality * baseline working * missing test file * more tests
332 lines
13 KiB
Go
332 lines
13 KiB
Go
// Package queryapi_test — Milestone 2 super-admin route tests.
|
|
//
|
|
// Routes covered:
|
|
// - GET /super-admin/bot/sessions?clientId=...
|
|
// - GET /super-admin/bot/sessions/{sessionId}?clientId=...
|
|
// - DELETE /super-admin/bot/sessions/{sessionId}?clientId=...
|
|
//
|
|
// Compile contract: this file references the generated method names
|
|
// (ListBotSessionsAsSuperAdmin, GetBotSessionAsSuperAdmin,
|
|
// DeleteBotSessionAsSuperAdmin) and Params types backend-eng emits when
|
|
// the M2 spec lands. Until then the file fails to compile — the failing
|
|
// state the team-lead M2 dispatch endorsed.
|
|
//
|
|
// Note: operationIds are fixed by the team-lead M2 dispatch
|
|
// (ListBotSessionsAsSuperAdmin / GetBotSessionAsSuperAdmin /
|
|
// DeleteBotSessionAsSuperAdmin); no rename is pending.
|
|
package queryapi_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
queryapi "queryorchestration/api/queryAPI"
|
|
"queryorchestration/internal/test"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestSuperAdminListSessions_RequiresClientId: missing the clientId
|
|
// query param returns 400. Plan §3: super-admin list takes a required
|
|
// `clientId=...`.
|
|
func TestSuperAdminListSessions_RequiresClientId(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
ctx, _ := newBotContext(t, http.MethodGet, "/super-admin/bot/sessions", nil)
|
|
// ClientId left empty.
|
|
err := cons.ListBotSessionsAsSuperAdmin(ctx, queryapi.ListBotSessionsAsSuperAdminParams{})
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.Truef(t, ok, "expected *echo.HTTPError, got %T: %v", err, err)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code,
|
|
"super-admin list without clientId must return 400")
|
|
}
|
|
|
|
// TestSuperAdminListSessions_VisibleAcrossUsers: super-admin sees U1's
|
|
// AND U2's sessions in clientA when listing with clientId=clientA.
|
|
// Plan §9: super-admin reads filter on client_id and is_deleted=false
|
|
// only — no created_by predicate.
|
|
func TestSuperAdminListSessions_VisibleAcrossUsers(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "BOT_M2_SA_LIST"
|
|
resetClientForBotTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Bot M2 SA List")
|
|
|
|
// Two sessions per user.
|
|
u1Created := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
u2Created := seedBotSessionViaService(t, cons, clientID, botOtherActorSubject)
|
|
|
|
path := fmt.Sprintf("/super-admin/bot/sessions?clientId=%s", clientID)
|
|
ctx, rec := newBotContext(t, http.MethodGet, path, nil)
|
|
|
|
require.NoError(t, cons.ListBotSessionsAsSuperAdmin(ctx,
|
|
queryapi.ListBotSessionsAsSuperAdminParams{ClientId: clientID}))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.BotSessionListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
|
|
ids := map[openapi_types.UUID]bool{}
|
|
for _, s := range resp.Sessions {
|
|
ids[s.Id] = true
|
|
}
|
|
assert.Truef(t, ids[u1Created.Id], "super-admin list must include U1's session %s", u1Created.Id)
|
|
assert.Truef(t, ids[u2Created.Id], "super-admin list must include U2's session %s", u2Created.Id)
|
|
}
|
|
|
|
// TestSuperAdminListSessions_DeletedHidden: by default,
|
|
// is_deleted=true sessions are excluded. Plan §10 M2 does not expose an
|
|
// includeDeleted query parameter for M2.
|
|
func TestSuperAdminListSessions_DeletedHidden(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "BOT_M2_SA_DEL"
|
|
resetClientForBotTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Bot M2 SA Del")
|
|
|
|
// Seed two sessions, soft-delete one.
|
|
keep := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
deleted := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
_, err := cfg.GetDBPool().Exec(t.Context(),
|
|
`UPDATE bot_sessions SET is_deleted = true WHERE id = $1`,
|
|
uuid.UUID(deleted.Id))
|
|
require.NoError(t, err)
|
|
|
|
path := fmt.Sprintf("/super-admin/bot/sessions?clientId=%s", clientID)
|
|
ctx, rec := newBotContext(t, http.MethodGet, path, nil)
|
|
require.NoError(t, cons.ListBotSessionsAsSuperAdmin(ctx,
|
|
queryapi.ListBotSessionsAsSuperAdminParams{ClientId: clientID}))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.BotSessionListResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
|
|
gotIDs := map[openapi_types.UUID]bool{}
|
|
for _, s := range resp.Sessions {
|
|
gotIDs[s.Id] = true
|
|
}
|
|
assert.Truef(t, gotIDs[keep.Id], "non-deleted session %s must appear", keep.Id)
|
|
assert.Falsef(t, gotIDs[deleted.Id], "is_deleted=true session %s must be hidden", deleted.Id)
|
|
}
|
|
|
|
// TestSuperAdminGetSession_RequiresClientIdAndId: super-admin can GET
|
|
// any session in clientA when calling with clientId=clientA. Missing
|
|
// clientId returns 400.
|
|
func TestSuperAdminGetSession_RequiresClientIdAndId(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "BOT_M2_SA_GET"
|
|
resetClientForBotTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Bot M2 SA Get")
|
|
|
|
created := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
|
|
t.Run("missing_client_id_400", func(t *testing.T) {
|
|
path := fmt.Sprintf("/super-admin/bot/sessions/%s", created.Id)
|
|
ctx, _ := newBotContext(t, http.MethodGet, path, nil)
|
|
err := cons.GetBotSessionAsSuperAdmin(ctx, asUUID(t, created.Id),
|
|
queryapi.GetBotSessionAsSuperAdminParams{})
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.Truef(t, ok, "expected *echo.HTTPError, got %T: %v", err, err)
|
|
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
|
|
})
|
|
|
|
t.Run("with_client_id_returns_session", func(t *testing.T) {
|
|
path := fmt.Sprintf("/super-admin/bot/sessions/%s?clientId=%s", created.Id, clientID)
|
|
ctx, rec := newBotContext(t, http.MethodGet, path, nil)
|
|
require.NoError(t, cons.GetBotSessionAsSuperAdmin(ctx, asUUID(t, created.Id),
|
|
queryapi.GetBotSessionAsSuperAdminParams{ClientId: clientID}))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp queryapi.BotSessionResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
assert.Equal(t, created.Id, resp.Id)
|
|
assert.Equal(t, clientID, resp.ClientId)
|
|
})
|
|
}
|
|
|
|
// TestSuperAdminGetSession_CrossClient_404: passing clientId=clientB
|
|
// for a session belonging to clientA returns 404.
|
|
func TestSuperAdminGetSession_CrossClient_404(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const (
|
|
clientA = "BOT_M2_SA_X_A"
|
|
clientB = "BOT_M2_SA_X_B"
|
|
)
|
|
resetClientForBotTests(t, cfg, clientA)
|
|
resetClientForBotTests(t, cfg, clientB)
|
|
test.CreateTestClient(t, cfg, clientA, "Bot M2 SA X A")
|
|
test.CreateTestClient(t, cfg, clientB, "Bot M2 SA X B")
|
|
|
|
created := seedBotSessionViaService(t, cons, clientA, testActorSubject)
|
|
|
|
path := fmt.Sprintf("/super-admin/bot/sessions/%s?clientId=%s", created.Id, clientB)
|
|
ctx, _ := newBotContext(t, http.MethodGet, path, nil)
|
|
err := cons.GetBotSessionAsSuperAdmin(ctx, asUUID(t, created.Id),
|
|
queryapi.GetBotSessionAsSuperAdminParams{ClientId: clientB})
|
|
httpErr, ok := err.(*echo.HTTPError)
|
|
require.Truef(t, ok, "expected *echo.HTTPError, got %T: %v", err, err)
|
|
assert.Equal(t, http.StatusNotFound, httpErr.Code,
|
|
"super-admin GET with mismatched clientId must be 404")
|
|
}
|
|
|
|
// TestSuperAdminDeleteSession_SoftDeletes: DELETE marks is_deleted=true;
|
|
// the row remains in the table; subsequent owner GET returns 404;
|
|
// subsequent super-admin GET also returns 404 (default filter).
|
|
func TestSuperAdminDeleteSession_SoftDeletes(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "BOT_M2_SA_DEL_SOFT"
|
|
resetClientForBotTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Bot M2 SA Del Soft")
|
|
|
|
created := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
|
|
// DELETE.
|
|
delPath := fmt.Sprintf("/super-admin/bot/sessions/%s?clientId=%s", created.Id, clientID)
|
|
delCtx, delRec := newBotContext(t, http.MethodDelete, delPath, nil)
|
|
require.NoError(t, cons.DeleteBotSessionAsSuperAdmin(delCtx, asUUID(t, created.Id),
|
|
queryapi.DeleteBotSessionAsSuperAdminParams{ClientId: clientID}))
|
|
// Per existing super-admin DELETE patterns in this repo, 204 No Content
|
|
// is the canonical success code for soft-delete-only endpoints.
|
|
assert.Equal(t, http.StatusNoContent, delRec.Code,
|
|
"DELETE must return 204 No Content for soft-delete success")
|
|
|
|
// Row must still exist with is_deleted=true.
|
|
var isDeleted bool
|
|
err := cfg.GetDBPool().QueryRow(t.Context(),
|
|
`SELECT is_deleted FROM bot_sessions WHERE id = $1`,
|
|
uuid.UUID(created.Id)).Scan(&isDeleted)
|
|
require.NoError(t, err, "row must remain in bot_sessions after soft-delete")
|
|
assert.True(t, isDeleted, "is_deleted must be flipped to true")
|
|
|
|
// Owner GET -> 404.
|
|
getPath := fmt.Sprintf("/client/%s/bot/sessions/%s", clientID, created.Id)
|
|
ownerCtx, _ := newBotContext(t, http.MethodGet, getPath, nil)
|
|
ownerErr := cons.GetBotSession(ownerCtx, clientID, asUUID(t, created.Id))
|
|
httpErr, ok := ownerErr.(*echo.HTTPError)
|
|
require.Truef(t, ok, "owner GET expected *echo.HTTPError, got %T: %v", ownerErr, ownerErr)
|
|
assert.Equal(t, http.StatusNotFound, httpErr.Code,
|
|
"owner GET must miss after super-admin soft-delete")
|
|
|
|
// Super-admin GET (default filter) -> 404.
|
|
saPath := fmt.Sprintf("/super-admin/bot/sessions/%s?clientId=%s", created.Id, clientID)
|
|
saCtx, _ := newBotContext(t, http.MethodGet, saPath, nil)
|
|
saErr := cons.GetBotSessionAsSuperAdmin(saCtx, asUUID(t, created.Id),
|
|
queryapi.GetBotSessionAsSuperAdminParams{ClientId: clientID})
|
|
httpErrSA, ok := saErr.(*echo.HTTPError)
|
|
require.Truef(t, ok, "super-admin GET expected *echo.HTTPError, got %T: %v", saErr, saErr)
|
|
assert.Equal(t, http.StatusNotFound, httpErrSA.Code,
|
|
"super-admin GET default filter must hide soft-deleted rows")
|
|
}
|
|
|
|
// TestSuperAdminDeleteSession_TurnsBecomeSessionDeleted: deleting a
|
|
// session causes any in-flight turns to transition to session_deleted
|
|
// status with appropriate error JSON. Plan §6 cross-references this
|
|
// behavior for the AddTurn flow; here we assert it triggers from the
|
|
// delete handler.
|
|
//
|
|
// Note: the M2 plan does not explicitly mandate this side-effect at
|
|
// the delete handler level (it is described in plan §6 as the AddTurn
|
|
// tx2 path). If backend-eng's design defers this to M4, the assertion
|
|
// will fail and the team-lead can adjudicate whether to defer.
|
|
func TestSuperAdminDeleteSession_TurnsBecomeSessionDeleted(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
cfg := &ControllerConfig{}
|
|
test.CreateDB(t, cfg)
|
|
initializeTestConfig(t, cfg)
|
|
|
|
svc := createControllerServices(cfg)
|
|
cons := queryapi.NewControllers(svc, cfg)
|
|
|
|
const clientID = "BOT_M2_SA_DEL_TURNS"
|
|
resetClientForBotTests(t, cfg, clientID)
|
|
test.CreateTestClient(t, cfg, clientID, "Bot M2 SA Del Turns")
|
|
|
|
created := seedBotSessionViaService(t, cons, clientID, testActorSubject)
|
|
// Seed one in-flight turn directly via SQL (M2 has no turn endpoint).
|
|
pool := cfg.GetDBPool()
|
|
_, err := pool.Exec(t.Context(), `
|
|
INSERT INTO bot_turns (session_id, ordinal, prompt, status, attempt_id)
|
|
VALUES ($1, 1, 'p1', 'in_flight', $2)
|
|
`, uuid.UUID(created.Id), uuid.New())
|
|
require.NoError(t, err)
|
|
|
|
// DELETE the session.
|
|
delPath := fmt.Sprintf("/super-admin/bot/sessions/%s?clientId=%s", created.Id, clientID)
|
|
delCtx, _ := newBotContext(t, http.MethodDelete, delPath, nil)
|
|
require.NoError(t, cons.DeleteBotSessionAsSuperAdmin(delCtx, asUUID(t, created.Id),
|
|
queryapi.DeleteBotSessionAsSuperAdminParams{ClientId: clientID}))
|
|
|
|
// The in-flight turn must transition to session_deleted with an
|
|
// error JSON payload containing the documented code.
|
|
var status string
|
|
var errJSON []byte
|
|
err = pool.QueryRow(t.Context(), `
|
|
SELECT status, error::text FROM bot_turns
|
|
WHERE session_id = $1 AND ordinal = 1
|
|
`, uuid.UUID(created.Id)).Scan(&status, &errJSON)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "session_deleted", status,
|
|
"in-flight turn must transition to session_deleted on session delete")
|
|
assert.NotEmpty(t, errJSON,
|
|
"session_deleted row must carry error JSON per plan §4 CHECK constraint")
|
|
}
|