package queryapi_test import ( "encoding/json" "net/http" "net/http/httptest" "testing" queryapi "queryorchestration/api/queryAPI" "queryorchestration/internal/cognitoauth" "queryorchestration/internal/database/repository" "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" ) // createDeleteContext creates an Echo context with DELETE method and admin user info set. // This simulates an authenticated admin making a delete request. func createDeleteContext(t testing.TB) (echo.Context, *httptest.ResponseRecorder) { t.Helper() e := echo.New() req := httptest.NewRequest(http.MethodDelete, "/", nil) rec := httptest.NewRecorder() ctx := e.NewContext(req, rec) ctx.Set("user_info", cognitoauth.UserInfo{ Email: "admin@test.local", Username: "admin", Groups: []string{"admin"}, }) return ctx, rec } // seedClientWithDocument creates a client, a root folder (via CreateTestClient), a sub-folder, // and a document with an S3 entry. Returns the document ID and sub-folder ID. func seedClientWithDocument(t *testing.T, cfg *ControllerConfig, clientID string) (uuid.UUID, uuid.UUID) { t.Helper() ctx := t.Context() q := cfg.GetDBQueries() test.CreateTestClient(t, cfg, clientID, "Test Client "+clientID) // Get root folder created by CreateTestClient rootFolder, err := q.GetFolderByPath(ctx, &repository.GetFolderByPathParams{ Clientid: clientID, Path: "/", }) require.NoError(t, err) // Create a sub-folder subFolder, err := q.CreateFolder(ctx, &repository.CreateFolderParams{ Path: "/test-folder", Clientid: clientID, Createdby: "admin@test.local", Parentid: &rootFolder.ID, }) require.NoError(t, err) // Create a document in the sub-folder filename := "test-doc.pdf" docID, err := q.CreateDocument(ctx, &repository.CreateDocumentParams{ Clientid: clientID, Hash: "hash-" + clientID, Filename: &filename, Folderid: &subFolder.ID, }) require.NoError(t, err) // Add an S3 entry for the document err = q.AddDocumentEntry(ctx, &repository.AddDocumentEntryParams{ Documentid: docID, Bucket: "test-bucket", Key: "test-key/" + clientID + "/doc.pdf", }) require.NoError(t, err) return docID, subFolder.ID } func TestDeleteDocument_Success(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-doc-success" docID, _ := seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteDocument(ctx, openapi_types.UUID(docID), queryapi.DeleteDocumentParams{}) require.NoError(t, err) assert.Equal(t, http.StatusNoContent, rec.Code) assert.Empty(t, rec.Body.String()) // Verify document is actually gone _, dbErr := cfg.GetDBQueries().GetDocumentSummary(t.Context(), docID) require.Error(t, dbErr) } // ptrBool returns a pointer to a bool value. func ptrBool(b bool) *bool { return &b } func TestDeleteDocument_Verbose(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-doc-verbose" docID, _ := seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteDocument(ctx, openapi_types.UUID(docID), queryapi.DeleteDocumentParams{ Verbose: ptrBool(true), }) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var resp queryapi.DeleteResponse err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) require.Len(t, resp.DeletedDocuments, 1) assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId) assert.Contains(t, resp.DeletedDocuments[0].S3Path, "s3://test-bucket/test-key/") } func TestDeleteDocument_NotFound(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) ctx, rec := createDeleteContext(t) fakeID := uuid.New() err := cons.DeleteDocument(ctx, openapi_types.UUID(fakeID), queryapi.DeleteDocumentParams{}) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) var resp queryapi.ErrorMessage err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) assert.Contains(t, resp.Message, "not found") } func TestDeleteClient_MissingConfirm(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-client-noconfirm" test.CreateTestClient(t, cfg, clientID, "Test Client") ctx, rec := createDeleteContext(t) err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{ Confirm: false, }) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) var resp queryapi.ErrorMessage err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) assert.Contains(t, resp.Message, "confirm=true") } func TestDeleteClient_Success(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-client-ok" seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{ Confirm: true, }) require.NoError(t, err) assert.Equal(t, http.StatusNoContent, rec.Code) assert.Empty(t, rec.Body.String()) // Verify client is gone _, dbErr := cfg.GetDBQueries().GetClient(t.Context(), clientID) require.Error(t, dbErr) } func TestDeleteClient_Verbose(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-client-verbose" docID, _ := seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteClient(ctx, queryapi.ClientID(clientID), queryapi.DeleteClientParams{ Confirm: true, Verbose: ptrBool(true), }) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var resp queryapi.DeleteResponse err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) require.Len(t, resp.DeletedDocuments, 1) assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId) assert.Contains(t, resp.DeletedDocuments[0].S3Path, "s3://test-bucket/") } func TestDeleteFolder_Success(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-folder-ok" test.CreateTestClient(t, cfg, clientID, "Test Client") q := cfg.GetDBQueries() rootFolder, err := q.GetFolderByPath(t.Context(), &repository.GetFolderByPathParams{ Clientid: clientID, Path: "/", }) require.NoError(t, err) subFolder, err := q.CreateFolder(t.Context(), &repository.CreateFolderParams{ Path: "/empty-folder", Clientid: clientID, Createdby: "admin@test.local", Parentid: &rootFolder.ID, }) require.NoError(t, err) ctx, rec := createDeleteContext(t) err = cons.DeleteFolder(ctx, openapi_types.UUID(subFolder.ID), queryapi.DeleteFolderParams{}) require.NoError(t, err) assert.Equal(t, http.StatusNoContent, rec.Code) assert.Empty(t, rec.Body.String()) // Verify folder is gone _, dbErr := q.GetFolderByID(t.Context(), subFolder.ID) require.Error(t, dbErr) } func TestDeleteFolder_RootFolderConflict(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-folder-root" test.CreateTestClient(t, cfg, clientID, "Test Client") q := cfg.GetDBQueries() rootFolder, err := q.GetFolderByPath(t.Context(), &repository.GetFolderByPathParams{ Clientid: clientID, Path: "/", }) require.NoError(t, err) ctx, rec := createDeleteContext(t) err = cons.DeleteFolder(ctx, openapi_types.UUID(rootFolder.ID), queryapi.DeleteFolderParams{}) require.NoError(t, err) assert.Equal(t, http.StatusConflict, rec.Code) var resp queryapi.ErrorMessage err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) assert.Contains(t, resp.Message, "root folder") } func TestDeleteFolder_DocumentsWithoutInclude(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-folder-nodocs" _, folderID := seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteFolder(ctx, openapi_types.UUID(folderID), queryapi.DeleteFolderParams{}) require.NoError(t, err) assert.Equal(t, http.StatusConflict, rec.Code) var resp queryapi.ErrorMessage err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) assert.Contains(t, resp.Message, "documents") } func TestDeleteFolder_IncludeDocumentsCascade(t *testing.T) { cfg := &ControllerConfig{} test.CreateDB(t, cfg) initializeTestConfig(t, cfg) svc := createControllerServices(cfg) cons := queryapi.NewControllers(svc, cfg) clientID := "del-folder-cascade" docID, folderID := seedClientWithDocument(t, cfg, clientID) ctx, rec := createDeleteContext(t) err := cons.DeleteFolder(ctx, openapi_types.UUID(folderID), queryapi.DeleteFolderParams{ IncludeDocuments: ptrBool(true), Verbose: ptrBool(true), }) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var resp queryapi.DeleteResponse err = json.Unmarshal(rec.Body.Bytes(), &resp) require.NoError(t, err) require.Len(t, resp.DeletedDocuments, 1) assert.Equal(t, openapi_types.UUID(docID), resp.DeletedDocuments[0].DocumentId) // Verify both folder and document are gone q := cfg.GetDBQueries() _, dbErr := q.GetFolderByID(t.Context(), folderID) require.Error(t, dbErr) _, dbErr = q.GetDocumentSummary(t.Context(), docID) require.Error(t, dbErr) }