Files
raven-test-integration/DataList/DataListOperations.cs
2026-02-28 19:53:34 -08:00

219 lines
10 KiB
C#

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
// -----------------------------------------------------------------------
/// <summary>
/// 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
/// </summary>
[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<long>();
Id.Should().BeGreaterThan(0);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(filterName);
a.ObjectResponse["data"]["public"].Value<bool>().Should().BeTrue();
// GET by id
a = await Util.GetAsync($"data-list-filter/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(filterName);
a.ObjectResponse["data"]["listKey"].Value<string>().Should().Be(ListKey);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// 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<string>().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<long>() == 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<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["public"].Value<bool>().Should().BeFalse();
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
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);
}
/// <summary>
/// A saved filter with DefaultFilter=true must be rejected — only the server
/// can create default filters internally.
/// </summary>
[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