using System; using Xunit; using Newtonsoft.Json.Linq; using FluentAssertions; namespace raven_integration { /* DataList tests — three layers: 1. SavedFilterCrud (LIVE — runs now) Exercises POST/GET/PUT/DELETE on /data-list-filter plus the /list endpoint. Uses WorkOrderDataList as the list key since work orders always have seeded data. 2. FilterAndSort stubs (SKIPPED — need real payloads from browser) To fill these in: a) Open the AyaNova UI, navigate to the Work Orders grid b) Apply a filter (e.g. "Notes contains fox"), open browser DevTools -> Network c) Find the POST to /data-list — copy the full request body d) Use that body as the starting point for the test payload here See Util.BuildDataListRequestEx and Util.BuildSimpleFilterDataListViewColumn for helpers. 3. Rights stub (SKIPPED — needs a user with no list rights) Verify that a user without the WorkOrder list role gets 403. ------ Date filter note: Server accounts for user's time zone offset when filtering by date range. All seeded users share the same tz offset (TIME_ZONE_ADJUSTMENT in util.cs). Use ToOffsetAdjustedUniversalTime() extension when building date filter values. */ public class DataListOperations { // ----------------------------------------------------------------------- // 1. SAVED FILTER CRUD // ----------------------------------------------------------------------- /// /// Create, retrieve, list, update, and delete a saved filter. /// Also verifies public/private visibility rules: /// - a public filter IS visible to other users /// - a private filter is NOT visible to other users /// [Fact] public async Task SavedFilterCRUD() { var token = await Util.GetTokenAsync("BizAdmin"); const string ListKey = "WorkOrderDataList"; // CREATE a public saved filter // UserId=0 — the server sets the actual user id from the auth token var filterName = Util.Uniquify("Test Saved Filter"); var payload = $$""" {"id":0,"concurrency":0,"userId":0,"name":"{{filterName}}","public":true,"defaultFilter":false,"listKey":"{{ListKey}}","filter":null} """; ApiResponse a = await Util.PostAsync("data-list-filter", token, payload); Util.ValidateDataReturnResponseOk(a); long Id = a.ObjectResponse["data"]["id"].Value(); Id.Should().BeGreaterThan(0); a.ObjectResponse["data"]["name"].Value().Should().Be(filterName); a.ObjectResponse["data"]["public"].Value().Should().BeTrue(); // GET by id a = await Util.GetAsync($"data-list-filter/{Id}", token); Util.ValidateDataReturnResponseOk(a); a.ObjectResponse["data"]["name"].Value().Should().Be(filterName); a.ObjectResponse["data"]["listKey"].Value().Should().Be(ListKey); var concurrency = a.ObjectResponse["data"]["concurrency"].Value(); // A different user should see a PUBLIC filter var otherToken = await Util.GetTokenAsync("SubContractorRestricted"); a = await Util.GetAsync($"data-list-filter/{Id}", otherToken); Util.ValidateDataReturnResponseOk(a); a.ObjectResponse["data"]["name"].Value().Should().Be(filterName); // LIST — filter should appear in the list for its key a = await Util.GetAsync($"data-list-filter/list?ListKey={ListKey}", token); Util.ValidateDataReturnResponseOk(a); ((JArray)a.ObjectResponse["data"]).Should().Contain( z => z["id"].Value() == Id, "the saved filter should appear in the list for its key"); // PUT — make it private and rename var updatedName = Util.Uniquify("Updated Saved Filter"); var putPayload = $$""" {"id":{{Id}},"concurrency":{{concurrency}},"userId":0,"name":"{{updatedName}}","public":false,"defaultFilter":false,"listKey":"{{ListKey}}","filter":null} """; a = await Util.PutAsync("data-list-filter", token, putPayload); Util.ValidateHTTPStatusCode(a, 200); // Verify the update a = await Util.GetAsync($"data-list-filter/{Id}", token); Util.ValidateDataReturnResponseOk(a); a.ObjectResponse["data"]["name"].Value().Should().Be(updatedName); a.ObjectResponse["data"]["public"].Value().Should().BeFalse(); var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value(); newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped"); // The other user should NO LONGER see the now-private filter a = await Util.GetAsync($"data-list-filter/{Id}", otherToken); Util.ValidateResponseNotFound(a); // DELETE a = await Util.DeleteAsync($"data-list-filter/{Id}", token); Util.ValidateHTTPStatusCode(a, 204); // Confirm deleted a = await Util.GetAsync($"data-list-filter/{Id}", token); Util.ValidateResponseNotFound(a); } /// /// A saved filter with DefaultFilter=true must be rejected — only the server /// can create default filters internally. /// [Fact] public async Task SavedFilter_DefaultFilterCannotBeCreatedByClient() { var token = await Util.GetTokenAsync("BizAdmin"); var payload = """ {"id":0,"concurrency":0,"userId":0,"name":"Should Fail","public":false,"defaultFilter":true,"listKey":"WorkOrderDataList","filter":null} """; ApiResponse a = await Util.PostAsync("data-list-filter", token, payload); Util.ValidateHTTPStatusCode(a, 400); a.ObjectResponse["error"].Should().NotBeNull(); } // ----------------------------------------------------------------------- // 2. FILTER AND SORT — stubs awaiting real payloads from the browser // ----------------------------------------------------------------------- // // How to capture a real payload: // 1. Run the server locally (dotnet run from raven/server/AyaNova/) // 2. Open the UI, navigate to Work Orders grid // 3. Apply a filter through the UI (e.g. Notes "contains" something) // 4. DevTools > Network — find the POST to /api/v8/data-list // 5. Copy the full request body JSON // 6. Use Util.BuildDataListRequestEx() or paste directly as a raw string // // Field keys for WorkOrderDataList (see WorkOrderDataList.cs in server): // "WorkOrderSerialNumber" integer (is the row id field) // "Customer" string // "WorkOrderServiceDate" datetime // "WorkOrderCloseByDate" datetime // "WorkOrderStatus" pick list / string // "Project" string // "WorkOrderAge" integer // // Operator constants are on Util: OpContains, OpStartsWith, OpEndsWith, // OpEquality, OpNotEqual, OpGreaterThan, OpLessThan, etc. // // Sort direction string: "+" ascending, "-" descending // // NOTE on date filters: use DateTime.Now.ToOffsetAdjustedUniversalTime() // (extension in util.cs) to avoid DST/timezone mismatch failures. // ----------------------------------------------------------------------- [Fact(Skip = "TODO: Capture real list payload from browser — see comment above")] public void WorkOrderList_StringContainsFilterWorks() { // Example shape once confirmed via browser: // // var token = await Util.GetTokenAsync("BizAdmin"); // dynamic dListView = new JArray(); // dListView.Add(Util.BuildSimpleFilterDataListViewColumn("Customer", Util.OpContains, "Acme")); // string body = Util.BuildDataListRequestEx(dListView, limit: 50, offset: 0, dataListKey: "WorkOrderDataList"); // ApiResponse a = await Util.PostAsync("data-list", token, body); // Util.ValidateDataReturnResponseOk(a); // var rows = (JArray)a.ObjectResponse["data"]; // rows.Should().NotBeEmpty(); // // each row should have "Acme" somewhere in the Customer column value } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_StringStartsWithFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_StringEqualsFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_StringNotEqualFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_DateRangeFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_NullFieldFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_MultiConditionAndFilterWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_SortAscendingWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_SortDescendingWorks() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_PaginationOffsetAndLimitWork() { } [Fact(Skip = "TODO: Capture real list payload from browser")] public void WorkOrderList_ReturnFormatMatchesExpectedShape() { } // ----------------------------------------------------------------------- // 3. RIGHTS // ----------------------------------------------------------------------- [Fact(Skip = "TODO: Identify a seeded user with no WorkOrder list rights and use their login")] public void WorkOrderList_UserWithoutListRightsGets403() { } }//eoc }//eons