Merged in feature/chatbot1 (pull request #223)
Chatbot functionality * baseline working * missing test file * more tests
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user