Compare commits

..

6 Commits

Author SHA1 Message Date
27fa1efdd1 4648 2026-03-02 15:51:01 -08:00
48f9f82c23 4648 2026-03-02 13:18:30 -08:00
d248753984 4648 2026-03-02 12:06:52 -08:00
db593886a3 4648 2026-02-28 19:53:34 -08:00
59cb886adb 4648 2026-02-27 16:31:35 -08:00
ca56c07ec1 4648 - all tests passing 2026-02-27 14:35:31 -08:00
21 changed files with 2275 additions and 4926 deletions

View File

@@ -1,7 +1,17 @@
{
"permissions": {
"allow": [
"Bash(dotnet test:*)"
"Bash(dotnet test:*)",
"Bash(grep -c \"\\\\[Fact\\\\]\" /c/data/code/raven-test-integration/**/*.cs)",
"Bash(grep -c \"Skip = \" /c/data/code/raven-test-integration/**/*.cs)",
"Bash(grep -l \"public class.*Controller\" /c/data/code/raven/server/AyaNova/Controllers/*.cs)",
"Bash(xargs -I {} basename {})",
"Bash(sed 's/Controller.cs//')",
"Bash(find /c/data/code/raven-test-integration -name \"*.cs\" -type f ! -path \"*/obj/*\" -exec grep -l \"public class.*Test\" {} ;)",
"Bash(xargs grep \"public class\")",
"Read(//c/data/code/raven-test-integration/**)",
"Bash(sed 's/.cs$//')",
"Bash(dotnet build raven-integration.csproj)"
]
}
}

95
Contract/ContractCrud.cs Normal file
View File

@@ -0,0 +1,95 @@
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class ContractCrud
{
/// <summary>
/// Full CRUD for a Contract, including a concurrency violation check.
/// Contract has a richer required-field set than most objects (billing override
/// type enums, response time, etc.) so this test exercises the full model round-trip.
///
/// ContractOverrideType enum: PriceDiscount = 1, CostMarkup = 2
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// -------------------------------------------------------------------
// CREATE
// responseTime: TimeSpan JSON format is "HH:MM:SS"
// partsOverrideType / serviceRatesOverrideType / travelRatesOverrideType:
// 1 = PriceDiscount, 2 = CostMarkup
// -------------------------------------------------------------------
var name = Util.Uniquify("Test Contract");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":"The quick brown fox jumped over the six lazy dogs!","wiki":null,"customFields":"{}","tags":[],"responseTime":"01:00:00","contractServiceRatesOnly":false,"contractTravelRatesOnly":false,"partsOverridePct":10.0,"partsOverrideType":1,"serviceRatesOverridePct":5.0,"serviceRatesOverrideType":1,"travelRatesOverridePct":0.0,"travelRatesOverrideType":1,"alertNotes":"Contract alert text"}
""";
ApiResponse a = await Util.PostAsync("contract", 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(name);
a.ObjectResponse["data"]["partsOverrideType"].Value<int>().Should().Be(1);
// -------------------------------------------------------------------
// GET
// -------------------------------------------------------------------
a = await Util.GetAsync($"contract/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["alertNotes"].Value<string>().Should().Be("Contract alert text");
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// PUT — Contract PUT returns the full updated object (not just concurrency)
// -------------------------------------------------------------------
var updatedName = Util.Uniquify("Updated Contract");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":"Updated notes","wiki":null,"customFields":"{}","tags":[],"responseTime":"02:00:00","contractServiceRatesOnly":false,"contractTravelRatesOnly":false,"partsOverridePct":15.0,"partsOverrideType":2,"serviceRatesOverridePct":0.0,"serviceRatesOverrideType":1,"travelRatesOverridePct":5.0,"travelRatesOverrideType":1,"alertNotes":"Updated alert"}
""";
a = await Util.PutAsync("contract", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
// PUT returns the full updated contract object
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["partsOverrideType"].Value<int>().Should().Be(2, "should have changed to CostMarkup");
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped");
// Verify update persisted
a = await Util.GetAsync($"contract/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["contractServiceRatesOnly"].Value<bool>().Should().BeFalse();
// -------------------------------------------------------------------
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
// -------------------------------------------------------------------
a = await Util.PutAsync("contract", token, putPayload); // putPayload has the old concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// DELETE
// -------------------------------------------------------------------
a = await Util.DeleteAsync($"contract/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
//bugbug case 4652 TO be fixed for this test to pass
//bug at server causing 500 to be returned instead of 404
//do not want to fix yet until ready for refactor in general
//deployed UI unaffected as it wouldn't attempt this normally
//this problem likely exhibits in any other object that has a 'populatedisplayfields' call at the server
//in their get routine that doesn't guard against null record
a = await Util.GetAsync($"contract/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

116
Customer/CustomerCrud.cs Normal file
View File

@@ -0,0 +1,116 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class CustomerCrud
{
/// <summary>
/// Full CRUD for a Customer, including concurrency violation and alert retrieval.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Customer");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":"The quick brown fox jumped over the six lazy dogs!","wiki":null,"customFields":"{}","tags":[],"webAddress":null,"alertNotes":"Test alert text for this customer","billHeadOffice":false,"headOfficeId":null,"techNotes":"Tech-only notes","accountNumber":"ACC-001","contractId":null,"contractExpires":null,"phone1":"555-1234","phone2":null,"phone3":null,"phone4":null,"phone5":null,"emailAddress":"test@example.com","postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("customer", 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(name);
// GET
a = await Util.GetAsync($"customer/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["phone1"].Value<string>().Should().Be("555-1234");
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// GET ALERT
a = await Util.GetAsync($"customer/alert/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"].Value<string>().Should().Be("Test alert text for this customer");
// PUT (update name and phone)
var updatedName = Util.Uniquify("Updated Customer");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":"Updated notes","wiki":null,"customFields":"{}","tags":[],"webAddress":null,"alertNotes":"Updated alert text","billHeadOffice":false,"headOfficeId":null,"techNotes":null,"accountNumber":null,"contractId":null,"contractExpires":null,"phone1":"555-9999","phone2":null,"phone3":null,"phone4":null,"phone5":null,"emailAddress":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("customer", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"customer/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["phone1"].Value<string>().Should().Be("555-9999");
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("customer", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"customer/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"customer/{Id}", token);
Util.ValidateResponseNotFound(a);
}
/// <summary>
/// A customer that has at least one work order associated with it should not
/// be deletable — referential integrity must be enforced.
/// </summary>
[Fact]
public async Task CustomerWithWorkOrders_CannotBeDeleted()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Create a dedicated customer so we control what is linked to it
var name = Util.Uniquify("RefInt Customer");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"webAddress":null,"alertNotes":null,"billHeadOffice":false,"headOfficeId":null,"techNotes":null,"accountNumber":null,"contractId":null,"contractExpires":null,"phone1":null,"phone2":null,"phone3":null,"phone4":null,"phone5":null,"emailAddress":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("customer", token, payload);
Util.ValidateDataReturnResponseOk(a);
long customerId = a.ObjectResponse["data"]["id"].Value<long>();
// Create a work order linked to this customer
var isoNow = DateTime.UtcNow.ToString("o");
var woPayload = $$"""
{"id":0,"concurrency":0,"serial":0,"notes":"RefInt test WO","wiki":null,"customFields":"{}","tags":[],"customerId":{{customerId}},"projectId":null,"contractId":null,"internalReferenceNumber":null,"customerReferenceNumber":null,"customerContactName":null,"fromQuoteId":null,"fromPMId":null,"serviceDate":"{{isoNow}}","completeByDate":null,"durationToCompleted":"00:00:00","invoiceNumber":null,"onsite":true,"customerSignature":null,"customerSignatureName":null,"customerSignatureCaptured":null,"techSignature":null,"techSignatureName":null,"techSignatureCaptured":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null,"isDirty":true,"isLockedAtServer":false}
""";
a = await Util.PostAsync("workorder", token, woPayload);
Util.ValidateDataReturnResponseOk(a);
long woId = a.ObjectResponse["data"]["id"].Value<long>();
// Attempt to delete the customer — should be blocked by referential integrity
//bugbug: this is returning a 500 due to a bug at the server end
//case 4653, holding off fixing until ready for refactor.
a = await Util.DeleteAsync($"customer/{customerId}", token);
Util.ValidateViolatesReferentialIntegrityError(a);
// Clean up: delete the work order first, then the customer
a = await Util.DeleteAsync($"workorder/{woId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"customer/{customerId}", token);
Util.ValidateHTTPStatusCode(a, 204);
}
}//eoc
}//eons

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,811 @@
using FluentAssertions;
using Newtonsoft.Json.Linq;
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
/*
Make fresh data list tests in here, exercise sorting, filtering, crud, rights saved filters, whatever could break
To save time instead of doing it the old way from before laboriously, just copy payloads from the client doing these things
with fresh seeded data and ensure anything important doesn't break on refactor
There could be some date issues from vague memory and note below so keeping that here for reference
and keeping old tests until I implement this as a walkthrough to confirm if there is something I might have missed to add here
DataList tests — three layers:
Just need to know datalists won't break on refactor, nothing fancy here but it was such a black hole of bugs in v7 I think I went overboard with the original
integration tests but the v8 system is more bulletproof, likely don't need as many tests as I used during development.
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 (LIVE — each test creates its own named filter, uses it, then deletes it)
Pattern:
a) CreateFilterAsync() — POST to /data-list-filter with filter rules as a JSON string
b) BuildDataListRequest() — POST body for /data-list with the filterId
c) Assert on the rows returned
d) Delete the filter in a finally block
For sort tests: POST /data-list-column-view/sort, then reset in finally.
For column-view test: POST /data-list-column-view, then reset in finally.
3. Rights (LIVE — SubContractor user has no WorkOrder list rights)
------
old potential BUGBUG: Server takes into account user's time zone offset when filtering lists by date range but here the local test runner just uses the windows system offset instead of the defined offset in the User account at the server
Fix: Since seeder uses same time zone for all users it generates then can simply fetch one single users' tz offset and use that centerally to calculate a relative now and relative today
same as the server does but in a central location here for all tests to use.
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.
*/
WorkOrderDataList column keys (default order):
"WorkOrderSerialNumber" integer / rid
"Customer" string
"WorkOrderServiceDate" datetime
"WorkOrderCloseByDate" datetime
"WorkOrderStatus" pick list / string
"Project" string
"WorkOrderAge" integer
Default sort: WorkOrderSerialNumber descending ("-").
*/
public class DataListOperations
{
// -----------------------------------------------------------------------
// PRIVATE HELPERS
// -----------------------------------------------------------------------
[Fact(Skip = "TODO: Implement validation test for DataLists")]
public void DataLists_ShouldSortFilterAllTypesAndSaveUpdate()
/// <summary>
/// Build a single column filter rule for use in the data-list-filter "filter" field.
/// any=false → AND, any=true → OR across the items list.
/// </summary>
private static JObject BuildFilterRule(string column, bool any,
params (string op, string value)[] items)
{
return new JObject
{
["column"] = column,
["any"] = any,
["items"] = new JArray(items.Select(i => new JObject { ["op"] = i.op, ["value"] = i.value }))
};
}
/// <summary>
/// POST a new private named filter to /data-list-filter and return its id.
/// Pass null filterRules to create an empty (no-op) filter that returns all records.
/// </summary>
private static async Task<long> CreateFilterAsync(string token, string listKey,
JArray filterRules)
{
dynamic d = new JObject();
d.id = 0;
d.concurrency = 0;
d.name = Util.Uniquify("Test Filter");
d["public"] = true;
d.defaultFilter = false;
d.listKey = listKey;
d.filter = filterRules?.ToString(Newtonsoft.Json.Formatting.None) ?? "[]";
ApiResponse a = await Util.PostAsync("data-list-filter", token,
d.ToString(Newtonsoft.Json.Formatting.None));
Util.ValidateDataReturnResponseOk(a);
return a.ObjectResponse["data"]["id"].Value<long>();
}
/// <summary>
/// Build the POST body for /data-list.
/// clientTimeStamp is set to DateTimeOffset.Now so relative date keywords work correctly.
/// </summary>
private static string BuildDataListRequest(long filterId, int offset = 0, int limit = 100)
{
var ts = DateTimeOffset.Now.ToString("o");
return $$"""{"offset":{{offset}},"limit":{{limit}},"dataListKey":"WorkOrderDataList","filterId":{{filterId}},"clientTimeStamp":"{{ts}}"}""";
}
/// <summary>
/// Decode the user id from the JWT auth token payload (avoids a round-trip GET).
/// The server embeds "id" as a string claim in the token.
/// </summary>
private static long GetUserIdFromToken(string token)
{
var payloadB64 = token.Split('.')[1].Replace('-', '+').Replace('_', '/');
while (payloadB64.Length % 4 != 0) payloadB64 += "=";
var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payloadB64));
return JObject.Parse(json)["id"].Value<long>();
}
/// <summary>
/// Look up the zero-based column index by field key from the data-list response object.
/// </summary>
private static int GetColumnIndex(JToken response, string fieldKey)
{
var columns = (JArray)response["columns"];
for (int i = 0; i < columns.Count; i++)
{
if (columns[i]["fk"]?.Value<string>() == fieldKey)
return i;
}
throw new InvalidOperationException($"Column '{fieldKey}' not found in response columns");
}
/// <summary>
/// Create a minimal work order with the given service date and return its id.
/// customerId=1 is XYZ Accounting in the seed data.
/// </summary>
private static async Task<long> CreateWorkOrderAsync(string token, DateTime serviceDate,
long customerId = 1)
{
var isoDate = serviceDate.ToString("o");
var payload = $$"""{"id":0,"concurrency":0,"serial":0,"notes":"DataList test WO","wiki":null,"customFields":"{}","tags":[],"customerId":{{customerId}},"projectId":null,"contractId":null,"internalReferenceNumber":null,"customerReferenceNumber":null,"customerContactName":null,"fromQuoteId":null,"fromPMId":null,"serviceDate":"{{isoDate}}","completeByDate":null,"durationToCompleted":"00:00:00","invoiceNumber":null,"onsite":false,"customerSignature":null,"customerSignatureName":null,"customerSignatureCaptured":null,"techSignature":null,"techSignatureName":null,"techSignatureCaptured":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null,"isDirty":true,"isLockedAtServer":false}""";
ApiResponse a = await Util.PostAsync("workorder", token, payload);
Util.ValidateDataReturnResponseOk(a);
return a.ObjectResponse["data"]["id"].Value<long>();
}
// -----------------------------------------------------------------------
// 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":"[]"}
""";
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":"[]"}
""";
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
// -----------------------------------------------------------------------
//
// Each test creates a named filter, POSTs to /data-list with that filterId,
// asserts the result, then deletes the filter in a finally block.
//
// Filter format stored in the "filter" field of data-list-filter:
// JSON array string: [{"column":"FIELD","any":false,"items":[{"op":"OP","value":"VAL"}]}]
// any=false → AND, any=true → OR
//
// Special server-side date keywords (use with OpEquality):
// "*yesterday*", "*tomorrow*", "*thisyear*", "*NULL*"
// -----------------------------------------------------------------------
/// <summary>
/// Customer name contains "XYZ Accounting" — all returned rows must have that substring.
/// Relies on seeded work orders for customerId=1 (XYZ Accounting).
/// </summary>
[Fact]
public async Task WorkOrderList_StringContainsFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var filterRules = new JArray { BuildFilterRule("Customer", false,
(Util.OpContains, "XYZ Accounting")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
Util.ValidateHTTPStatusCode(a, 200);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("seeded data has work orders for XYZ Accounting");
int customerIdx = GetColumnIndex(a.ObjectResponse, "Customer");
foreach (var row in rows)
row[customerIdx]["v"].Value<string>().Should()
.Contain("XYZ Accounting", "contains filter must only return matching rows");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Customer name starts with "XY" — all returned rows must have that prefix.
/// </summary>
[Fact]
public async Task WorkOrderList_StringStartsWithFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var filterRules = new JArray { BuildFilterRule("Customer", false,
(Util.OpStartsWith, "XY")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("seeded data has customers starting with 'XY'");
int customerIdx = GetColumnIndex(a.ObjectResponse, "Customer");
foreach (var row in rows)
row[customerIdx]["v"].Value<string>().Should()
.StartWith("XY", "startswith filter must only return matching rows");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Customer name equals "XYZ Accounting" exactly — all returned rows must match exactly.
/// </summary>
[Fact]
public async Task WorkOrderList_StringEqualsFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var filterRules = new JArray { BuildFilterRule("Customer", false,
(Util.OpEquality, "XYZ Accounting")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("seeded data has work orders for XYZ Accounting");
int customerIdx = GetColumnIndex(a.ObjectResponse, "Customer");
foreach (var row in rows)
row[customerIdx]["v"].Value<string>().Should()
.Be("XYZ Accounting", "equality filter must only return exact matches");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Customer name NOT equal to "XYZ Accounting" — no returned rows may have that customer.
/// </summary>
[Fact]
public async Task WorkOrderList_StringNotEqualFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var filterRules = new JArray { BuildFilterRule("Customer", false,
(Util.OpNotEqual, "XYZ Accounting")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId, limit: 50));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("many seeded work orders belong to other customers");
int customerIdx = GetColumnIndex(a.ObjectResponse, "Customer");
foreach (var row in rows)
row[customerIdx]["v"].Value<string>().Should()
.NotBe("XYZ Accounting", "not-equal filter must exclude that customer");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Date range AND filter: service date > 2 days ago AND < 2 days hence.
/// Creates two work orders (yesterday noon, tomorrow noon) and verifies both appear.
/// </summary>
[Fact]
public async Task WorkOrderList_DateRangeOrMultiConditionFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Create two WOs with service dates clearly inside a 4-day window
var yesterday = DateTime.Today.AddDays(-1).AddHours(12);
var tomorrow = DateTime.Today.AddDays(1).AddHours(12);
long wo1Id = await CreateWorkOrderAsync(token, yesterday);
long wo2Id = await CreateWorkOrderAsync(token, tomorrow);
// Filter: service date > 2 days ago AND < 2 days hence (any=false = AND)
var from = DateTime.Now.AddDays(-2).ToOffsetAdjustedUniversalTime();
var to = DateTime.Now.AddDays(2).ToOffsetAdjustedUniversalTime();
var filterRules = new JArray { BuildFilterRule("WorkOrderServiceDate", false,
(Util.OpGreaterThan, from.ToString("o")),
(Util.OpLessThan, to.ToString("o"))) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("date range filter must return rows within the window");
int serialIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderSerialNumber");
var returnedIds = rows.Select(r => r[serialIdx]["i"].Value<long>()).ToHashSet();
returnedIds.Should().Contain(wo1Id, "yesterday's work order is within the date range");
returnedIds.Should().Contain(wo2Id, "tomorrow's work order is within the date range");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
await Util.DeleteAsync($"workorder/{wo1Id}", token);
await Util.DeleteAsync($"workorder/{wo2Id}", token);
}
}
/// <summary>
/// Date OR filter: service date = *yesterday* OR = *tomorrow*.
/// Creates two work orders and verifies both appear via server-side relative date keywords.
/// </summary>
[Fact]
public async Task WorkOrderList_DateRangeFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var yesterday = DateTime.Today.AddDays(-1).AddHours(12);
var tomorrow = DateTime.Today.AddDays(1).AddHours(12);
long wo1Id = await CreateWorkOrderAsync(token, yesterday);
long wo2Id = await CreateWorkOrderAsync(token, tomorrow);
// any=true → OR across items
var filterRules = new JArray { BuildFilterRule("WorkOrderServiceDate", true,
(Util.OpEquality, "*yesterday*"),
(Util.OpEquality, "*tomorrow*")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("OR date filter must return rows matching either day");
int serialIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderSerialNumber");
var returnedIds = rows.Select(r => r[serialIdx]["i"].Value<long>()).ToHashSet();
returnedIds.Should().Contain(wo1Id, "yesterday's work order should appear");
returnedIds.Should().Contain(wo2Id, "tomorrow's work order should appear");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
await Util.DeleteAsync($"workorder/{wo1Id}", token);
await Util.DeleteAsync($"workorder/{wo2Id}", token);
}
}
/// <summary>
/// Filter for rows where WorkOrderStatus IS NULL (*NULL* keyword).
/// Seeded data contains many work orders with no status assigned.
/// </summary>
[Fact]
public async Task WorkOrderList_NullFieldFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var filterRules = new JArray { BuildFilterRule("WorkOrderStatus", false,
(Util.OpEquality, "*NULL*")) };
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("seeded data should have work orders with no status");
int statusIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderStatus");
foreach (var row in rows)
row[statusIdx]["v"].Type.Should().Be(JTokenType.Null,
"null filter must only return rows where status is null");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Multi-column AND filter: Customer contains "ou" AND service date = *thisyear*.
/// Both conditions must be satisfied (any=false per column, multiple columns = AND between them).
/// </summary>
[Fact]
public async Task WorkOrderList_MultiConditionAndFilterWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Two separate column filters — server ANDs them together
var filterRules = new JArray
{
BuildFilterRule("Customer", false, (Util.OpContains, "ou")),
BuildFilterRule("WorkOrderServiceDate", false, (Util.OpEquality, "*thisyear*"))
};
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", filterRules);
try
{
ApiResponse a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty("seeded data has customers containing 'ou' with this year's work orders");
int serviceDateIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderServiceDate");
foreach (var row in rows)
{
var dateStr = row[serviceDateIdx]["v"]?.Value<string>();
if (dateStr != null)
DateTime.Parse(dateStr).Year.Should().Be(DateTime.Now.Year,
"service dates must all be in the current year");
}
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
}
}
/// <summary>
/// Set sort to WorkOrderServiceDate ascending via /data-list-column-view/sort,
/// then verify that returned rows are in ascending date order.
/// Resets sort to default (WorkOrderSerialNumber descending) in the finally block.
/// </summary>
[Fact]
public async Task WorkOrderList_SortAscendingWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Set sort ascending
var sortPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderServiceDate"],"sortDesc":[false]}""";
ApiResponse a = await Util.PostAsync("data-list-column-view/sort", token, sortPayload);
Util.ValidateHTTPStatusCode(a, 200);
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", null);
try
{
a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId, limit: 20));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty();
// Response sortBy should confirm ascending
a.ObjectResponse["sortBy"]["WorkOrderServiceDate"].Value<string>().Should().Be("+",
"sortBy in response should show ascending");
// Verify actual data order
int serviceDateIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderServiceDate");
DateTime? prev = null;
foreach (var row in rows)
{
var dateStr = row[serviceDateIdx]["v"]?.Value<string>();
if (dateStr == null) continue;
var date = DateTime.Parse(dateStr);
if (prev.HasValue)
date.Should().BeOnOrAfter(prev.Value, "rows must be in ascending service date order");
prev = date;
}
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
// Reset sort to default: WorkOrderSerialNumber descending
var resetPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderSerialNumber"],"sortDesc":[true]}""";
await Util.PostAsync("data-list-column-view/sort", token, resetPayload);
}
}
/// <summary>
/// Set sort to WorkOrderServiceDate descending, verify returned rows are in descending order.
/// Resets sort to default in the finally block.
/// </summary>
[Fact]
public async Task WorkOrderList_SortDescendingWorks()
{
var token = await Util.GetTokenAsync("BizAdmin");
var sortPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderServiceDate"],"sortDesc":[true]}""";
ApiResponse a = await Util.PostAsync("data-list-column-view/sort", token, sortPayload);
Util.ValidateHTTPStatusCode(a, 200);
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", null);
try
{
a = await Util.PostAsync("data-list", token, BuildDataListRequest(filterId, limit: 20));
Util.ValidateDataReturnResponseOk(a);
var rows = (JArray)a.ObjectResponse["data"];
rows.Should().NotBeEmpty();
a.ObjectResponse["sortBy"]["WorkOrderServiceDate"].Value<string>().Should().Be("-",
"sortBy in response should show descending");
int serviceDateIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderServiceDate");
DateTime? prev = null;
foreach (var row in rows)
{
var dateStr = row[serviceDateIdx]["v"]?.Value<string>();
if (dateStr == null) continue;
var date = DateTime.Parse(dateStr);
if (prev.HasValue)
date.Should().BeOnOrBefore(prev.Value, "rows must be in descending service date order");
prev = date;
}
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
var resetPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderSerialNumber"],"sortDesc":[true]}""";
await Util.PostAsync("data-list-column-view/sort", token, resetPayload);
}
}
/// <summary>
/// Verify that offset and limit produce non-overlapping pages with a consistent totalRecordCount.
/// Uses WorkOrderSerialNumber ascending sort so pages are deterministic.
/// </summary>
[Fact]
public async Task WorkOrderList_PaginationOffsetAndLimitWork()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Sort ascending so pages are predictable
var sortPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderSerialNumber"],"sortDesc":[false]}""";
await Util.PostAsync("data-list-column-view/sort", token, sortPayload);
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", null);
try
{
// Page 1: offset=0, limit=5
ApiResponse a = await Util.PostAsync("data-list", token,
BuildDataListRequest(filterId, offset: 0, limit: 5));
Util.ValidateDataReturnResponseOk(a);
var page1Rows = (JArray)a.ObjectResponse["data"];
page1Rows.Count.Should().Be(5, "page 1 should return exactly 5 rows");
long totalCount = a.ObjectResponse["totalRecordCount"].Value<long>();
totalCount.Should().BeGreaterThan(5, "total record count must exceed one page");
int serialIdx = GetColumnIndex(a.ObjectResponse, "WorkOrderSerialNumber");
var page1Ids = page1Rows.Select(r => r[serialIdx]["i"].Value<long>()).ToHashSet();
// Page 2: offset=5, limit=5
a = await Util.PostAsync("data-list", token,
BuildDataListRequest(filterId, offset: 5, limit: 5));
Util.ValidateDataReturnResponseOk(a);
var page2Rows = (JArray)a.ObjectResponse["data"];
page2Rows.Count.Should().Be(5, "page 2 should return exactly 5 rows");
a.ObjectResponse["totalRecordCount"].Value<long>().Should().Be(totalCount,
"totalRecordCount must be the same across pages");
var page2Ids = page2Rows.Select(r => r[serialIdx]["i"].Value<long>()).ToHashSet();
page1Ids.Should().NotIntersectWith(page2Ids, "page 1 and page 2 must not overlap");
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
// Reset sort to default
var resetPayload = """{"listKey":"WorkOrderDataList","sortBy":["WorkOrderSerialNumber"],"sortDesc":[true]}""";
await Util.PostAsync("data-list-column-view/sort", token, resetPayload);
}
}
/// <summary>
/// Verify that the data-list response has the expected shape:
/// data, totalRecordCount, columns, sortBy, filter, hiddenAffectiveColumns.
/// Also verifies that a custom column order set via /data-list-column-view is reflected.
/// Resets column view to default order in the finally block.
/// </summary>
[Fact]
public async Task WorkOrderList_ReturnFormatMatchesExpectedShape()
{
var token = await Util.GetTokenAsync("BizAdmin");
// Set a custom column order: Customer moves to position 0
long userId = GetUserIdFromToken(token);
dynamic cvPayload = new JObject();
cvPayload.userId = userId;
cvPayload.listKey = "WorkOrderDataList";
cvPayload.columns = "[\"Customer\",\"WorkOrderSerialNumber\",\"WorkOrderServiceDate\",\"WorkOrderCloseByDate\",\"WorkOrderStatus\",\"Project\",\"WorkOrderAge\"]";
cvPayload.sort = "{\"WorkOrderSerialNumber\":\"+\"}";
ApiResponse a = await Util.PostAsync("data-list-column-view", token,
cvPayload.ToString(Newtonsoft.Json.Formatting.None));
Util.ValidateHTTPStatusCode(a, 200);
long filterId = await CreateFilterAsync(token, "WorkOrderDataList", null);
try
{
a = await Util.PostAsync("data-list", token,
BuildDataListRequest(filterId, offset: 0, limit: 10));
Util.ValidateDataReturnResponseOk(a);
Util.ValidateHTTPStatusCode(a, 200);
// Top-level keys must all be present
a.ObjectResponse["data"].Should().NotBeNull();
a.ObjectResponse["totalRecordCount"].Should().NotBeNull();
a.ObjectResponse["columns"].Should().NotBeNull();
a.ObjectResponse["sortBy"].Should().NotBeNull();
a.ObjectResponse["filter"].Should().NotBeNull();
a.ObjectResponse["hiddenAffectiveColumns"].Should().NotBeNull();
var rows = (JArray)a.ObjectResponse["data"];
var columns = (JArray)a.ObjectResponse["columns"];
// Customer should now be in the first column position
columns[0]["fk"].Value<string>().Should().Be("Customer",
"custom column order should put Customer first");
// Every row must have exactly as many values as there are columns
foreach (var row in rows)
((JArray)row).Count.Should().Be(columns.Count,
"row value count must match column definition count");
a.ObjectResponse["totalRecordCount"].Value<long>().Should().BeGreaterThan(0);
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", token);
// Reset column view to default order
dynamic resetCv = new JObject();
resetCv.userId = userId;
resetCv.listKey = "WorkOrderDataList";
resetCv.columns = "[\"WorkOrderSerialNumber\",\"Customer\",\"WorkOrderServiceDate\",\"WorkOrderCloseByDate\",\"WorkOrderStatus\",\"Project\",\"WorkOrderAge\"]";
resetCv.sort = "{\"WorkOrderSerialNumber\":\"-\"}";
await Util.PostAsync("data-list-column-view", token,
resetCv.ToString(Newtonsoft.Json.Formatting.None));
}
}
// -----------------------------------------------------------------------
// 3. RIGHTS
// -----------------------------------------------------------------------
/// <summary>
/// A user with no roles (roles:0) must receive 403 when requesting WorkOrderDataList.
/// Creates a temporary user for isolation, deletes it in the finally block.
/// </summary>
[Fact]
public async Task WorkOrderList_UserWithoutListRightsGets403()
{
var adminToken = await Util.GetTokenAsync("BizAdmin");
// Create a temporary user with no roles (no spaces allowed in login names)
var login = Util.Uniquify("norights").Replace(" ", "");
dynamic userPayload = new JObject();
userPayload.name = login;
userPayload.active = true;
userPayload.allowLogin = true;
userPayload.login = login;
userPayload.password = login;
userPayload.roles = 0;
userPayload.userType = 2;
userPayload.notes = "temp test user";
userPayload.customFields = Util.UserRequiredCustomFieldsJsonString();
ApiResponse a = await Util.PostAsync("user", adminToken, userPayload.ToString(Newtonsoft.Json.Formatting.None));
Util.ValidateDataReturnResponseOk(a);
long tempUserId = a.ObjectResponse["data"]["id"].Value<long>();
// Create a public filter for the no-rights user to reference
long filterId = await CreateFilterAsync(adminToken, "WorkOrderDataList", null);
try
{
var noRightsToken = await Util.GetTokenAsync(login);
a = await Util.PostAsync("data-list", noRightsToken, BuildDataListRequest(filterId));
Util.ValidateErrorCodeResponse(a, 2004, 403);
}
finally
{
await Util.DeleteAsync($"data-list-filter/{filterId}", adminToken);
await Util.DeleteAsync($"user/{tempUserId}", adminToken);
}
}
}//eoc
}//eons

File diff suppressed because one or more lines are too long

View File

@@ -1,30 +0,0 @@
// using Xunit;
// using Newtonsoft.Json.Linq;
// using FluentAssertions;
// namespace raven_integration
// {
// public class DataListRights
// {
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task InsufficentRightsShouldNotRetrieve()
// {
// //Get without rights
// /*
// "{\"error\":{\"code\":\"2004\",\"message\":\"User not authorized for this resource operation (insufficient rights)\"}}"
// */
// //ApiResponse a = await Util.GetAsync("data-list/list?DataListKey=TestWidgetDataList&Offset=0&Limit=3", await Util.GetTokenAsync("CustomerRestricted"));
// ApiResponse a = await Util.PostAsync($"data-list", await Util.GetTokenAsync("CustomerRestricted"), Util.BuildDataListRequestEx());
// Util.ValidateErrorCodeResponse(a, 2004, 403);
// }
// //==================================================
// }//eoc
// }//eons

View File

@@ -1,413 +0,0 @@
// using System;
// using Xunit;
// using Newtonsoft.Json.Linq;
// using FluentAssertions;
// using System.Collections.Generic;
// using System.Collections.Concurrent;
// namespace raven_integration
// {
// public class DataListSorting
// {
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task DefaultSortByIdWorks()
// {
// var WidgetNameStart = Util.Uniquify("DefaultSortByIdWorks");
// //CREATE 3 TEST WIDGETS TO TEST ORDER
// long FirstInOrderWidgetId = 0;
// long SecondInOrderWidgetId = 0;
// long ThirdInOrderWidgetId = 0;
// dynamic w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.usertype = 1;
// ApiResponse a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// ThirdInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// SecondInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// FirstInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// //CREATE FILTER
// dynamic dListView = new JArray();
// //name starts with filter to constrict to widgets that this test block created only
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, WidgetNameStart));
// //NOW FETCH WIDGET LIST WITH FILTER
// // a = await Util.GetAsync($"data-list/list?DataListKey=TestWidgetDataList&Offset=0&Limit=999&DataFilterId={DataFilterId.ToString()}", await Util.GetTokenAsync("superuser", "l3tm3in"));
// a = await Util.PostAsync($"data-list", await Util.GetTokenAsync("superuser", "l3tm3in"), Util.BuildDataListRequestEx(dListView));
// Util.ValidateDataReturnResponseOk(a);
// Util.ValidateHTTPStatusCode(a, 200);
// //assert contains exactly 3 records
// ((JArray)a.ObjectResponse["data"]).Count.Should().Be(3);
// //assert the order returned
// a.ObjectResponse["data"][0][0]["i"].Value<long>().Should().Be(FirstInOrderWidgetId);
// a.ObjectResponse["data"][1][0]["i"].Value<long>().Should().Be(SecondInOrderWidgetId);
// a.ObjectResponse["data"][2][0]["i"].Value<long>().Should().Be(ThirdInOrderWidgetId);
// a = await Util.DeleteAsync("widget/" + FirstInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + SecondInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + ThirdInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// }
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task SortByFieldAscendingWorks()
// {
// var WidgetNameStart = Util.Uniquify("SortByFieldAscendingWorks");
// //CREATE 3 TEST WIDGETS TO TEST ORDER
// long FirstInOrderWidgetId = 0;
// long SecondInOrderWidgetId = 0;
// long ThirdInOrderWidgetId = 0;
// dynamic w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.startDate = DateTime.Now;
// w.endDate = DateTime.Now.AddHours(1);
// w.usertype = 1;
// ApiResponse a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// FirstInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.startDate = DateTime.Now.AddHours(1);
// w.endDate = DateTime.Now.AddHours(2);
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// SecondInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.startDate = DateTime.Now.AddHours(2);
// w.endDate = DateTime.Now.AddHours(3);
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// ThirdInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// //CREATE FILTER
// //FILTER IN BY NAME FOR TESTING THIS RUN ONLY
// dynamic dListView = new JArray();
// //name starts with filter to constrict to widgets that this test block created only
// //SORT ORDER ###################
// // dynamic dsortarray = new JArray();
// // dynamic dsort = new JObject();
// // dsort.fld = "widgetstartdate";
// // dsort.dir = "+";
// // dsortarray.Add(dsort);
// //both conditions filter and sort here
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, WidgetNameStart, "+"));
// //NOW FETCH WIDGET LIST WITH FILTER
// a = await Util.PostAsync($"data-list", await Util.GetTokenAsync("superuser", "l3tm3in"), Util.BuildDataListRequestEx(dListView));
// Util.ValidateDataReturnResponseOk(a);
// Util.ValidateHTTPStatusCode(a, 200);
// //assert contains exactly 3 records
// ((JArray)a.ObjectResponse["data"]).Count.Should().Be(3);
// //assert the order returned
// a.ObjectResponse["data"][0][0]["i"].Value<long>().Should().Be(FirstInOrderWidgetId);
// a.ObjectResponse["data"][1][0]["i"].Value<long>().Should().Be(SecondInOrderWidgetId);
// a.ObjectResponse["data"][2][0]["i"].Value<long>().Should().Be(ThirdInOrderWidgetId);
// a = await Util.DeleteAsync("widget/" + FirstInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + SecondInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + ThirdInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// }
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task SortByFieldDescendingWorks()
// {
// var WidgetNameStart = Util.Uniquify("SortByFieldDescendingWorks");
// //CREATE 3 TEST WIDGETS TO TEST ORDER
// long FirstInOrderWidgetId = 0;
// long SecondInOrderWidgetId = 0;
// long ThirdInOrderWidgetId = 0;
// dynamic w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.count = 999;
// w.usertype = 1;
// ApiResponse a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// FirstInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.count = 665;
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// SecondInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.count = 333;
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// ThirdInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// //CREATE FILTER
// //FILTER IN BY NAME FOR TESTING THIS RUN ONLY
// dynamic dListView = new JArray();
// //name starts with filter to constrict to widgets that this test block created only
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, WidgetNameStart));
// //SORT ORDER ###################
// // dynamic dsortarray = new JArray();
// // dynamic dsort = new JObject();
// // dsort.fld = "widgetcount";
// // dsort.dir = "-";
// dListView.Add(Util.BuildSimpleSortDataListViewColumn("widgetcount", "-"));
// //NOW FETCH WIDGET LIST WITH FILTER
// a = await Util.PostAsync($"data-list", await Util.GetTokenAsync("superuser", "l3tm3in"), Util.BuildDataListRequestEx(dListView));
// Util.ValidateDataReturnResponseOk(a);
// Util.ValidateHTTPStatusCode(a, 200);
// //assert contains exactly 3 records
// ((JArray)a.ObjectResponse["data"]).Count.Should().Be(3);
// //assert the order returned
// a.ObjectResponse["data"][0][0]["i"].Value<long>().Should().Be(FirstInOrderWidgetId);
// a.ObjectResponse["data"][1][0]["i"].Value<long>().Should().Be(SecondInOrderWidgetId);
// a.ObjectResponse["data"][2][0]["i"].Value<long>().Should().Be(ThirdInOrderWidgetId);
// a = await Util.DeleteAsync("widget/" + FirstInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + SecondInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + ThirdInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// }
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task SortByMultipleFieldsWorks()
// {
// /*
// Created order:
// dollaramount, count
// 2,1
// 1,2
// 2,2
// 1,1
// sorted order:
// dollar asc, count desc
// 1,2
// 1,1
// 2,2
// 2,1
// */
// var WidgetNameStart = Util.Uniquify("SortByMultipleFieldsWorks");
// //CREATE 4 TEST WIDGETS TO TEST ORDER
// long FirstInOrderWidgetId = 0;
// long SecondInOrderWidgetId = 0;
// long ThirdInOrderWidgetId = 0;
// long FourthInOrderWidgetId = 0;
// dynamic w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.dollaramount = 2.22;
// w.count = 1;
// w.usertype = 1;
// ApiResponse a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// FourthInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.dollaramount = 1.11;
// w.count = 2;
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// FirstInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.dollaramount = 1.11;
// w.count = 1;
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// SecondInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// w = new JObject();
// w.name = Util.Uniquify(WidgetNameStart);
// w.customFields = Util.WidgetRequiredCustomFieldsJsonString();
// w.notes = "blah";
// w.dollaramount = 2.22;
// w.count = 2;
// w.usertype = 1;
// a = await Util.PostAsync("widget", await Util.GetTokenAsync("superuser", "l3tm3in"), w.ToString());
// Util.ValidateDataReturnResponseOk(a);
// ThirdInOrderWidgetId = a.ObjectResponse["data"]["id"].Value<long>();
// //CREATE FILTER
// //FILTER IN BY NAME FOR TESTING THIS RUN ONLY
// dynamic dListView = new JArray();
// //name starts with filter to constrict to widgets that this test block created only
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, WidgetNameStart));
// //SORT ORDER ###################
// // dynamic dsortarray = new JArray();
// //First column
// // dynamic dsort1 = new JObject();
// // dsort1.fld = "widgetdollaramount";
// // dsort1.dir = "+";
// // dsortarray.Add(dsort1);
// dListView.Add(Util.BuildSimpleSortDataListViewColumn("widgetdollaramount", "+"));
// //Second column
// // dynamic dsort2 = new JObject();
// // dsort2.fld = "widgetcount";
// // dsort2.dir = "-";
// // dsortarray.Add(dsort2);
// dListView.Add(Util.BuildSimpleSortDataListViewColumn("widgetcount", "-"));
// //NOW FETCH WIDGET LIST WITH FILTER
// a = await Util.PostAsync($"data-list", await Util.GetTokenAsync("superuser", "l3tm3in"), Util.BuildDataListRequestEx(dListView));
// Util.ValidateDataReturnResponseOk(a);
// Util.ValidateHTTPStatusCode(a, 200);
// //assert contains exactly 3 records
// ((JArray)a.ObjectResponse["data"]).Count.Should().Be(4);
// //assert the order returned
// a.ObjectResponse["data"][0][0]["i"].Value<long>().Should().Be(FirstInOrderWidgetId);
// a.ObjectResponse["data"][1][0]["i"].Value<long>().Should().Be(SecondInOrderWidgetId);
// a.ObjectResponse["data"][2][0]["i"].Value<long>().Should().Be(ThirdInOrderWidgetId);
// a.ObjectResponse["data"][3][0]["i"].Value<long>().Should().Be(FourthInOrderWidgetId);
// a = await Util.DeleteAsync("widget/" + FirstInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + SecondInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + ThirdInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// a = await Util.DeleteAsync("widget/" + FourthInOrderWidgetId.ToString(), await Util.GetTokenAsync("superuser", "l3tm3in"));
// Util.ValidateHTTPStatusCode(a, 204);
// }
// //========================================================================
// }//eoc
// }//eons

View File

@@ -1,202 +0,0 @@
// using System;
// using Xunit;
// using Newtonsoft.Json.Linq;
// using FluentAssertions;
// using System.Collections.Generic;
// using System.Collections.Concurrent;
// namespace raven_integration
// {
// public class DataListFilterCrud
// {
// /// <summary>
// /// Test all CRUD routes
// /// </summary>
// [Fact]
// public async Task CRUD()
// {
// //CREATE
// dynamic d = new JObject();
// d.name = Util.Uniquify("Test DataListView");
// d["public"] = true;
// d.listKey="TestWidgetDataList";
// //"[{fld:"name",op:"!=",value:"Notequaltothis"},{fld:"tags",op:"Eq",value:"[23,456,54]"}]
// dynamic dListView = new JArray();
// // dynamic df = new JObject();
// // df.fld = "widgetname";
// // df.op = "%-";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dListView.Add(df);
// // d.filter = dListView.ToString();//it expects it to be a json string, not actual json
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, "Generic"));
// d.listView=dListView.ToString(Newtonsoft.Json.Formatting.None);
// ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString(Newtonsoft.Json.Formatting.None));
// Util.ValidateDataReturnResponseOk(a);
// long Id = a.ObjectResponse["data"]["id"].Value<long>();
// //RETRIEVE
// //Get one
// a = await Util.GetAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("BizAdmin"));
// Util.ValidateDataReturnResponseOk(a);
// a.ObjectResponse["data"]["name"].Value<string>().Should().StartWith("Test DataListView");
// //Get as alternate user should work for public filter
// a = await Util.GetAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("SubContractorRestricted"));
// Util.ValidateDataReturnResponseOk(a);
// a.ObjectResponse["data"]["name"].Value<string>().Should().StartWith("Test DataListView");
// //UPDATE
// //PUT, make private
// d["public"] = false;
// d.name = Util.Uniquify("Put - Test DataListView (privatized)");
// d.concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// a = await Util.PutAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateHTTPStatusCode(a, 200);
// //check PUT worked
// a = await Util.GetAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("BizAdmin"));
// Util.ValidateNoErrorInResponse(a);
// a.ObjectResponse["data"]["name"].Value<string>().Should().Be(d.name.ToString());
// //FETCH DISALLOWED
// //Get as alternate user should fail for private filter
// a = await Util.GetAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("SubContractorRestricted"));
// Util.ValidateResponseNotFound(a);
// // //DELETE
// ApiResponse DELETETestResponse = await Util.DeleteAsync("data-list-view/" + Id.ToString(), await Util.GetTokenAsync("BizAdmin"));
// Util.ValidateHTTPStatusCode(DELETETestResponse, 204);
// }
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task InvalidListKeyShouldFail()
// {
// //CREATE
// dynamic d = new JObject();
// d.name = Util.Uniquify("Test DataListView");
// d["public"] = true;
// d.listKey = "nonexistant";
// //"[{fld:"name",op:"!=",value:"Notequaltothis"},{fld:"tags",op:"Eq",value:"[23,456,54]"}]
// // dynamic dfilter = new JArray();
// // dynamic df = new JObject();
// // df.fld = "name";
// // df.op = "%-";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dfilter.Add(df);
// // d.filter = dfilter.ToString();//it expects it to be a json string, not actual json
// // ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// dynamic dListView = new JArray();
// // dynamic df = new JObject();
// // df.fld = "widgetname";
// // df.op = "%-";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dListView.Add(df);
// // d.filter = dListView.ToString();//it expects it to be a json string, not actual json
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("widgetname", Util.OpStartsWith, "Generic"));
// d.listView=dListView.ToString(Newtonsoft.Json.Formatting.None);
// ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString(Newtonsoft.Json.Formatting.None));
// Util.ValidateErrorCodeResponse(a, 2200, 400);
// Util.ShouldContainValidationError(a, "ListKey", "2203");
// }
// /// <summary>
// ///
// /// </summary>
// [Fact]
// public async Task InvalidFieldNameShouldFail()
// {
// //CREATE
// dynamic d = new JObject();
// d.name = Util.Uniquify("Test DataListView");
// d["public"] = true;
// d.listKey="TestWidgetDataList";
// //"[{fld:"name",op:"!=",value:"Notequaltothis"},{fld:"tags",op:"Eq",value:"[23,456,54]"}]
// // dynamic dfilter = new JArray();
// // dynamic df = new JObject();
// // df.fld = "doesntexist";
// // df.op = "%-";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dfilter.Add(df);
// // d.filter = dfilter.ToString();//it expects it to be a json string, not actual json
// // ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// dynamic dListView = new JArray();
// // dynamic df = new JObject();
// // df.fld = "widgetname";
// // df.op = "%-";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dListView.Add(df);
// // d.filter = dListView.ToString();//it expects it to be a json string, not actual json
// dListView.Add(Util.BuildSimpleFilterDataListViewColumn("doesntexist", Util.OpStartsWith, "Generic"));
// d.listView=dListView.ToString(Newtonsoft.Json.Formatting.None);
// ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString(Newtonsoft.Json.Formatting.None));
// Util.ValidateErrorCodeResponse(a, 2200, 400);
// Util.ShouldContainValidationError(a, "ListView", "2203");
// }
// //REMOVED THIS VALIDATION TEST AS SWITCH TO DATALISTVIEW NO LONGER VALIDATES FILTER ANYWAY
// //ONLY COLUMN NAMES AND LIST NAME
// // /// <summary>
// // ///
// // /// </summary>
// // [Fact]
// // public async Task InvalidOperatorShouldFail()
// // {
// // //CREATE
// // dynamic d = new JObject();
// // d.name = Util.Uniquify("Test DataListView");
// // d["public"] = true;
// // d.listKey="TestWidgetDataList";
// // //"[{fld:"name",op:"!=",value:"Notequaltothis"},{fld:"tags",op:"Eq",value:"[23,456,54]"}]
// // dynamic dfilter = new JArray();
// // dynamic df = new JObject();
// // df.fld = "name";
// // df.op = "wtf";
// // df.value = "Generic";//lots of seed widgets start with Generic
// // dfilter.Add(df);
// // d.filter = dfilter.ToString();//it expects it to be a json string, not actual json
// // ApiResponse a = await Util.PostAsync("data-list-view", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// // Util.ValidateErrorCodeResponse(a, 2200, 400);
// // Util.ShouldContainValidationError(a, "Filter", "2203");
// // }
// //==================================================
// }//eoc
// }//eons

65
Memo/MemoCrud.cs Normal file
View File

@@ -0,0 +1,65 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class MemoCrud
{
private static long GetUserIdFromToken(string token)
{
var payloadB64 = token.Split('.')[1].Replace('-', '+').Replace('_', '/');
while (payloadB64.Length % 4 != 0) payloadB64 += "=";
var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payloadB64));
return JObject.Parse(json)["id"].Value<long>();
}
/// <summary>
/// Create a memo, read it back, then delete it and confirm it is gone.
/// Memos are immutable after sending so there is no PUT step.
///
/// Note: the normal POST (users=[userId]) returns 202 Accepted with no body — the
/// server creates per-recipient records but returns no id. To get a testable memo id
/// we use the v8 migration compatibility path: users=[-7] authenticated as superuser
/// (id=1). That path creates a single memo and returns 200 with {data:{id:N}}.
/// </summary>
[Fact]
public async Task CreateReadDelete()
{
// Must be superuser (id=1) to use the migration compatibility path
var token = await Util.GetTokenAsync("superuser", "l3tm3in");
var isoNow = DateTime.UtcNow.ToString("o");
var subject = Util.Uniquify("Test Memo");
// CREATE via migration path: users=[-7], superuser auth → 200 with {data:{id:N}}
// fromId=1, toId=1 so that foreign-key constraints are satisfied
var payload = $$"""
{"memo":{"id":0,"concurrency":0,"name":"{{subject}}","notes":"Test memo body text.","wiki":null,"customFields":"{}","tags":[],"viewed":false,"replied":false,"fromId":1,"toId":1,"sent":"{{isoNow}}"},"users":[-7]}
""";
ApiResponse a = await Util.PostAsync("memo", token, payload);
Util.ValidateDataReturnResponseOk(a);
long Id = a.ObjectResponse["data"]["id"].Value<long>();
Id.Should().BeGreaterThan(0);
// GET
a = await Util.GetAsync($"memo/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(subject);
// DELETE
a = await Util.DeleteAsync($"memo/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"memo/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

View File

@@ -0,0 +1,88 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class NotificationOps
{
private static long GetUserIdFromToken(string token)
{
var payloadB64 = token.Split('.')[1].Replace('-', '+').Replace('_', '/');
while (payloadB64.Length % 4 != 0) payloadB64 += "=";
var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payloadB64));
return JObject.Parse(json)["id"].Value<long>();
}
/// <summary>
/// Send a direct-message notification to self, verify it appears in the app-notification
/// list, then delete it and confirm the count endpoint responds throughout.
/// </summary>
[Fact(Skip ="Works once every 12 hours due to bug at server case ")]
public async Task DirectMessage_SendAndDelete()
{
var token = await Util.GetTokenAsync("BizAdmin");
var userId = GetUserIdFromToken(token);
// GET new-count (baseline — count may be any value)
ApiResponse a = await Util.GetAsync("notify/new-count", token);
Util.ValidateDataReturnResponseOk(a);
// POST direct-message to self
var messageText = Util.Uniquify("Integration test notification");
var payload = $$"""
{"message":"{{messageText}}","users":[{{userId}}]}
""";
a = await Util.PostAsync("notify/direct-message", token, payload);
Util.ValidateHTTPStatusCode(a, 204); // 204 No Content — server sends but returns no body
// Poll app-notifications until the message appears.
// The server processes notify events on a background timer (20 s in debug,
// 60 s in release), so the message may not be visible immediately.
JToken matchedNotification = null;
var deadline = DateTime.UtcNow.AddSeconds(65);
while (DateTime.UtcNow < deadline)
{
a = await Util.GetAsync("notify/app-notifications", token);
Util.ValidateDataReturnResponseOk(a);
var notifications = (JArray)a.ObjectResponse["data"];
notifications.Should().NotBeNull("a JArray of notifications should be returned");
foreach (var notification in notifications)
{
var body = notification["body"]?.ToString() ?? string.Empty;
var message = notification["message"]?.ToString() ?? string.Empty;
if (body.Contains(messageText) || message.Contains(messageText))
{
matchedNotification = notification;
break;
}
}
if (matchedNotification != null) break;
await Task.Delay(3000);
}
matchedNotification.Should().NotBeNull("the direct message just sent should appear in app-notifications within 65 seconds");
long notificationId = matchedNotification["id"].Value<long>();
// GET new-count again (just verify the endpoint responds)
a = await Util.GetAsync("notify/new-count", token);
Util.ValidateDataReturnResponseOk(a);
// DELETE the notification
a = await Util.DeleteAsync($"notify/{notificationId}", token);
Util.ValidateHTTPStatusCode(a, 204);
}
}//eoc
}//eons

131
PM/PMCrud.cs Normal file
View File

@@ -0,0 +1,131 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class PMCrud
{
/// <summary>
/// Full CRUD for a Preventive Maintenance header + PMItem.
/// Also tests concurrency violation and id-from-number lookup.
/// PM is structurally parallel to WorkOrder (header → items → sub-types)
/// so failures here flag regressions if the refactor consolidates those patterns.
///
/// Key enum values used:
/// PMTimeUnit: Minutes=2, Hours=3, Days=4, Months=6, Years=7
/// AyaDaysOfWeek (flags): Monday=1, Tuesday=2, Wednesday=4, Thursday=8,
/// Friday=16, Saturday=32, Sunday=64
/// excludeDaysOfWeek=96 means Saturday(32)|Sunday(64)
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// nextServiceDate must be a future UTC timestamp
var isoNextService = DateTime.UtcNow.AddDays(7).ToString("o");
// -------------------------------------------------------------------
// CREATE PM HEADER
// customerId=1 is a seeded customer; serial=0 means server assigns it.
// -------------------------------------------------------------------
var payload = $$"""
{"id":0,"concurrency":0,"serial":0,"notes":"Test PM — the quick brown fox jumped over the six lazy dogs!","wiki":null,"customFields":"{}","tags":[],"copyWiki":false,"copyAttachments":true,"stopGeneratingDate":null,"excludeDaysOfWeek":96,"active":false,"nextServiceDate":"{{isoNextService}}","repeatUnit":6,"generateBeforeUnit":4,"repeatInterval":1,"generateBeforeInterval":3,"customerId":1,"projectId":null,"internalReferenceNumber":"PM-INT-001","customerReferenceNumber":null,"customerContactName":null,"onsite":true,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("pm", token, payload);
Util.ValidateDataReturnResponseOk(a);
long PMId = a.ObjectResponse["data"]["id"].Value<long>();
long PMSerial = a.ObjectResponse["data"]["serial"].Value<long>();
PMId.Should().BeGreaterThan(0);
PMSerial.Should().BeGreaterThan(0, "server should have assigned a non-zero serial");
// -------------------------------------------------------------------
// GET PM
// -------------------------------------------------------------------
a = await Util.GetAsync($"pm/{PMId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["id"].Value<long>().Should().Be(PMId);
a.ObjectResponse["data"]["repeatUnit"].Value<int>().Should().Be(6, "should be Months");
a.ObjectResponse["data"]["excludeDaysOfWeek"].Value<int>().Should().Be(96, "should be Sat+Sun flags");
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("PM-INT-001");
var headerConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// ID-FROM-NUMBER LOOKUP
// -------------------------------------------------------------------
a = await Util.GetAsync($"pm/id-from-number/{PMSerial}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"].Value<long>().Should().Be(PMId);
// -------------------------------------------------------------------
// CREATE PM ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":0,"concurrency":0,"notes":"Test PM item summary","wiki":null,"customFields":"{}","tags":[],"pmId":{{PMId}},"techNotes":"Tech notes for PM item","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PostAsync("pm/items", token, payload);
Util.ValidateDataReturnResponseOk(a);
long PMItemId = a.ObjectResponse["data"]["id"].Value<long>();
PMItemId.Should().BeGreaterThan(0);
// GET PM ITEM
a = await Util.GetAsync($"pm/items/{PMItemId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["notes"].Value<string>().Should().Be("Test PM item summary");
var itemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// UPDATE PM ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":{{PMItemId}},"concurrency":{{itemConcurrency}},"notes":"Updated PM item notes","wiki":null,"customFields":"{}","tags":[],"pmId":{{PMId}},"techNotes":"Updated tech notes","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PutAsync("pm/items/", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
var newItemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newItemConcurrency.Should().NotBe(itemConcurrency, "concurrency should increment on update");
// CONCURRENCY VIOLATION on item
a = await Util.PutAsync("pm/items/", token, payload); // stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// UPDATE PM HEADER
// -------------------------------------------------------------------
var isoNewNextService = DateTime.UtcNow.AddDays(14).ToString("o");
payload = $$"""
{"id":{{PMId}},"concurrency":{{headerConcurrency}},"serial":{{PMSerial}},"notes":"Updated PM notes","wiki":null,"customFields":"{}","tags":[],"copyWiki":true,"copyAttachments":true,"stopGeneratingDate":null,"excludeDaysOfWeek":0,"active":false,"nextServiceDate":"{{isoNewNextService}}","repeatUnit":7,"generateBeforeUnit":4,"repeatInterval":1,"generateBeforeInterval":5,"customerId":1,"projectId":null,"internalReferenceNumber":"PM-INT-002","customerReferenceNumber":null,"customerContactName":null,"onsite":false,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("pm", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("PM-INT-002");
a.ObjectResponse["data"]["repeatUnit"].Value<int>().Should().Be(7, "should now be Years");
var newHeaderConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newHeaderConcurrency.Should().NotBe(headerConcurrency);
// CONCURRENCY VIOLATION on header
a = await Util.PutAsync("pm", token, payload); // stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// DELETE in bottom-up order: item → header
// -------------------------------------------------------------------
a = await Util.DeleteAsync($"pm/items/{PMItemId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"pm/{PMId}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm header is gone
a = await Util.GetAsync($"pm/{PMId}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

119
Part/PartCrud.cs Normal file
View File

@@ -0,0 +1,119 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class PartCrud
{
/// <summary>
/// Full CRUD for a Part, including concurrency violation.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Part");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"cost":9.99,"retail":14.99,"description":null,"notes":null,"wiki":null,"customFields":"{}","tags":[],"manufacturerId":null,"manufacturerNumber":null,"wholeSalerId":null,"wholeSalerNumber":null,"alternativeWholeSalerId":null,"alternativeWholeSalerNumber":null,"unitOfMeasure":null,"upc":null}
""";
ApiResponse a = await Util.PostAsync("part", 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(name);
// GET
a = await Util.GetAsync($"part/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["cost"].Value<decimal>().Should().Be(9.99m);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update name and cost)
var updatedName = Util.Uniquify("Updated Part");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"cost":11.99,"retail":14.99,"description":null,"notes":null,"wiki":null,"customFields":"{}","tags":[],"manufacturerId":null,"manufacturerNumber":null,"wholeSalerId":null,"wholeSalerNumber":null,"alternativeWholeSalerId":null,"alternativeWholeSalerNumber":null,"unitOfMeasure":null,"upc":null}
""";
a = await Util.PutAsync("part", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"part/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["cost"].Value<decimal>().Should().Be(11.99m);
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("part", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"part/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"part/{Id}", token);
Util.ValidateResponseNotFound(a);
}
/// <summary>
/// Verifies that serial numbers can be written to and read back from a Part.
/// </summary>
[Fact]
public async Task Serials_RoundTrip()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE a part to attach serials to
var name = Util.Uniquify("Test Part");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"cost":9.99,"retail":14.99,"description":null,"notes":null,"wiki":null,"customFields":"{}","tags":[],"manufacturerId":null,"manufacturerNumber":null,"wholeSalerId":null,"wholeSalerNumber":null,"alternativeWholeSalerId":null,"alternativeWholeSalerNumber":null,"unitOfMeasure":null,"upc":null}
""";
ApiResponse a = await Util.PostAsync("part", token, payload);
Util.ValidateDataReturnResponseOk(a);
long Id = a.ObjectResponse["data"]["id"].Value<long>();
Id.Should().BeGreaterThan(0);
// GET serials — should be empty initially
a = await Util.GetAsync($"part/serials/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
Util.ValidateHTTPStatusCode(a, 200);
((JArray)a.ObjectResponse["data"]).Count.Should().Be(0);
// PUT serials — write three serial numbers
var serialsPayload = """["SN-A001","SN-A002","SN-A003"]""";
a = await Util.PutAsync($"part/serials/{Id}", token, serialsPayload);
Util.ValidateHTTPStatusCode(a, 200);
// GET serials again — verify all three are returned
a = await Util.GetAsync($"part/serials/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
Util.ValidateHTTPStatusCode(a, 200);
var serials = (JArray)a.ObjectResponse["data"];
serials.Count.Should().Be(3);
serials.Values<string>().Should().Contain("SN-A001");
serials.Values<string>().Should().Contain("SN-A002");
serials.Values<string>().Should().Contain("SN-A003");
// Clean up: clear serials before deleting (parts with serials cannot be deleted)
a = await Util.PutAsync($"part/serials/{Id}", token, "[]");
Util.ValidateHTTPStatusCode(a, 200);
a = await Util.DeleteAsync($"part/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
}
}//eoc
}//eons

134
Quote/QuoteCrud.cs Normal file
View File

@@ -0,0 +1,134 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class QuoteCrud
{
/// <summary>
/// Full CRUD for a Quote header + QuoteItem + QuoteItemLabor sub-type.
/// Also tests concurrency violation on the header and id-from-number lookup.
/// Mirrors the WorkOrderCrud structure — if a refactor consolidates WO/Quote
/// patterns, failures here flag the regression.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
var isoNow = DateTime.UtcNow.ToString("o");
var isoOneHourFromNow = DateTime.UtcNow.AddHours(1).ToString("o");
// -------------------------------------------------------------------
// CREATE QUOTE HEADER
// customerId=1 is a seeded customer; serial=0 means server assigns it.
// -------------------------------------------------------------------
var payload = $$"""
{"id":0,"concurrency":0,"serial":0,"notes":"Test quote — the quick brown fox jumped over the six lazy dogs!","wiki":null,"customFields":"{}","tags":[],"preparedById":null,"introduction":"Test introduction","requested":null,"validUntil":null,"submitted":null,"approved":null,"customerId":1,"projectId":null,"internalReferenceNumber":"INT-001","customerReferenceNumber":null,"customerContactName":null,"onsite":true,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("quote", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteId = a.ObjectResponse["data"]["id"].Value<long>();
long QuoteSerial = a.ObjectResponse["data"]["serial"].Value<long>();
QuoteId.Should().BeGreaterThan(0);
QuoteSerial.Should().BeGreaterThan(0, "server should have assigned a non-zero serial");
// -------------------------------------------------------------------
// GET QUOTE
// -------------------------------------------------------------------
a = await Util.GetAsync($"quote/{QuoteId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["id"].Value<long>().Should().Be(QuoteId);
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("INT-001");
var headerConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// ID-FROM-NUMBER LOOKUP
// -------------------------------------------------------------------
a = await Util.GetAsync($"quote/id-from-number/{QuoteSerial}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"].Value<long>().Should().Be(QuoteId);
// -------------------------------------------------------------------
// CREATE QUOTE ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":0,"concurrency":0,"notes":"Test quote item summary","wiki":null,"customFields":"{}","tags":[],"quoteId":{{QuoteId}},"techNotes":"Tech notes for item","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PostAsync("quote/items", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteItemId = a.ObjectResponse["data"]["id"].Value<long>();
QuoteItemId.Should().BeGreaterThan(0);
// GET QUOTE ITEM
a = await Util.GetAsync($"quote/items/{QuoteItemId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["notes"].Value<string>().Should().Be("Test quote item summary");
var itemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// CREATE QUOTE ITEM LABOR (nested sub-type)
// -------------------------------------------------------------------
payload = $$"""
{"id":0,"concurrency":0,"userId":null,"serviceStartDate":"{{isoNow}}","serviceStopDate":"{{isoOneHourFromNow}}","serviceRateId":null,"serviceDetails":"Test labor service details","serviceRateQuantity":1,"noChargeQuantity":0,"taxCodeSaleId":null,"priceOverride":null,"quoteItemId":{{QuoteItemId}}}
""";
a = await Util.PostAsync("quote/items/labors", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteItemLaborId = a.ObjectResponse["data"]["id"].Value<long>();
QuoteItemLaborId.Should().BeGreaterThan(0);
a.ObjectResponse["data"]["serviceDetails"].Value<string>().Should().Be("Test labor service details");
// -------------------------------------------------------------------
// UPDATE QUOTE ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":{{QuoteItemId}},"concurrency":{{itemConcurrency}},"notes":"Updated item notes","wiki":null,"customFields":"{}","tags":[],"quoteId":{{QuoteId}},"techNotes":"Updated tech notes","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PutAsync("quote/items/", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
var newItemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newItemConcurrency.Should().NotBe(itemConcurrency);
// CONCURRENCY VIOLATION on item
a = await Util.PutAsync("quote/items/", token, payload); // still has stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// UPDATE QUOTE HEADER
// -------------------------------------------------------------------
payload = $$"""
{"id":{{QuoteId}},"concurrency":{{headerConcurrency}},"serial":{{QuoteSerial}},"notes":"Updated quote notes","wiki":null,"customFields":"{}","tags":[],"preparedById":null,"introduction":"Updated introduction","requested":null,"validUntil":null,"submitted":null,"approved":null,"customerId":1,"projectId":null,"internalReferenceNumber":"INT-002","customerReferenceNumber":null,"customerContactName":null,"onsite":false,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("quote", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("INT-002");
var newHeaderConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newHeaderConcurrency.Should().NotBe(headerConcurrency);
// CONCURRENCY VIOLATION on header
a = await Util.PutAsync("quote", token, payload); // stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// DELETE in bottom-up order: labor → item → header
// -------------------------------------------------------------------
a = await Util.DeleteAsync($"quote/items/labors/{QuoteItemLaborId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"quote/items/{QuoteItemId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"quote/{QuoteId}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm header is gone
a = await Util.GetAsync($"quote/{QuoteId}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

199
Reference/ReferenceCrud.cs Normal file
View File

@@ -0,0 +1,199 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class ServiceRateCrud
{
/// <summary>
/// Full CRUD for a ServiceRate, including concurrency violation and 404 on deleted record.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Service Rate");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"accountNumber":null,"cost":50.00,"charge":75.00,"unit":null,"contractOnly":false}
""";
ApiResponse a = await Util.PostAsync("service-rate", 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(name);
// GET
a = await Util.GetAsync($"service-rate/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["cost"].Value<decimal>().Should().Be(50.00m);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update name and charge)
var updatedName = Util.Uniquify("Updated Service Rate");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"accountNumber":null,"cost":50.00,"charge":80.00,"unit":null,"contractOnly":false}
""";
a = await Util.PutAsync("service-rate", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"service-rate/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["charge"].Value<decimal>().Should().Be(80.00m);
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("service-rate", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"service-rate/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"service-rate/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
public class TravelRateCrud
{
/// <summary>
/// Full CRUD for a TravelRate, including concurrency violation and 404 on deleted record.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Travel Rate");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"accountNumber":null,"cost":50.00,"charge":75.00,"unit":null,"contractOnly":false}
""";
ApiResponse a = await Util.PostAsync("travel-rate", 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(name);
// GET
a = await Util.GetAsync($"travel-rate/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["cost"].Value<decimal>().Should().Be(50.00m);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update name and charge)
var updatedName = Util.Uniquify("Updated Travel Rate");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"accountNumber":null,"cost":50.00,"charge":80.00,"unit":null,"contractOnly":false}
""";
a = await Util.PutAsync("travel-rate", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"travel-rate/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["charge"].Value<decimal>().Should().Be(80.00m);
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("travel-rate", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"travel-rate/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"travel-rate/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
public class TaxCodeCrud
{
/// <summary>
/// Full CRUD for a TaxCode, including concurrency violation and 404 on deleted record.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Tax Code");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"taxAPct":5.0,"taxBPct":2.5,"taxOnTax":false}
""";
ApiResponse a = await Util.PostAsync("tax-code", 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(name);
// GET
a = await Util.GetAsync($"tax-code/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["taxAPct"].Value<decimal>().Should().Be(5.0m);
a.ObjectResponse["data"]["taxBPct"].Value<decimal>().Should().Be(2.5m);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update name only; taxAPct and taxBPct stay the same)
var updatedName = Util.Uniquify("Updated Tax Code");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"taxAPct":5.0,"taxBPct":2.5,"taxOnTax":false}
""";
a = await Util.PutAsync("tax-code", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"tax-code/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["taxAPct"].Value<decimal>().Should().Be(5.0m);
a.ObjectResponse["data"]["taxBPct"].Value<decimal>().Should().Be(2.5m);
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("tax-code", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"tax-code/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"tax-code/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

60
Schedule/ScheduleReads.cs Normal file
View File

@@ -0,0 +1,60 @@
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class ScheduleReads
{
/// <summary>
/// Verify the service schedule endpoint returns a well-formed response for a 7-day window
/// </summary>
[Fact]
public async Task ServiceSchedule_ReturnsSuccessfully()
{
var token = await Util.GetTokenAsync("BizAdmin");
var start = DateTime.Today.ToString("o");
var end = DateTime.Today.AddDays(7).ToString("o");
var payload = $$"""
{"view":2,"start":"{{start}}","end":"{{end}}","wisuColorSource":2,"tags":[],"dark":false}
""";
ApiResponse a = await Util.PostAsync("schedule/svc", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
Util.ValidateDataReturnResponseOk(a);
// Verify the response shape — items must be present as a JArray (may be empty if no WOs are scheduled)
a.ObjectResponse["data"]["items"].Should().NotBeNull("schedule/svc response data should contain an items key");
a.ObjectResponse["data"]["items"].Should().BeOfType<JArray>("items should be a JArray");
}
/// <summary>
/// Verify the personal schedule endpoint returns a well-formed response for a 7-day window
/// </summary>
[Fact]
public async Task PersonalSchedule_ReturnsSuccessfully()
{
var token = await Util.GetTokenAsync("BizAdmin");
var start = DateTime.Today.ToString("o");
var end = DateTime.Today.AddDays(7).ToString("o");
var payload = $$"""
{"view":2,"start":"{{start}}","end":"{{end}}","wisuColorSource":2,"wisu":false,"reviews":false,"reminders":false,"dark":false,"userId":0}
""";
ApiResponse a = await Util.PostAsync("schedule/personal", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
Util.ValidateDataReturnResponseOk(a);
}
//==================================================
}//eoc
}//eons

130
Unit/UnitCrud.cs Normal file
View File

@@ -0,0 +1,130 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class UnitModelCrud
{
/// <summary>
/// Full CRUD for a UnitModel, including concurrency violation.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Unit Model");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"vendorId":null,"upc":null,"lifeTimeWarranty":false,"introducedDate":null,"discontinued":false,"discontinuedDate":null,"warrantyLength":null,"warrantyTerms":null}
""";
ApiResponse a = await Util.PostAsync("unit-model", 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(name);
// GET
a = await Util.GetAsync($"unit-model/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update name)
var updatedName = Util.Uniquify("Updated Unit Model");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"vendorId":null,"upc":null,"lifeTimeWarranty":false,"introducedDate":null,"discontinued":false,"discontinuedDate":null,"warrantyLength":null,"warrantyTerms":null}
""";
a = await Util.PutAsync("unit-model", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"unit-model/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("unit-model", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"unit-model/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"unit-model/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
public class UnitCrud
{
/// <summary>
/// Full CRUD for a Unit, including concurrency violation.
/// Uses customerId=1, the always-safe seeded customer.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var serial = Util.Uniquify("SN").Replace(" ", "");
var payload = $$"""
{"id":0,"concurrency":0,"active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"serial":"{{serial}}","customerId":1,"parentUnitId":null,"unitModelId":null,"unitHasOwnAddress":false,"boughtHere":false,"purchasedFromVendorId":null,"receipt":null,"purchasedDate":null,"description":null,"replacedByUnitId":null,"overrideModelWarranty":false,"warrantyLength":null,"warrantyTerms":null,"contractId":null,"contractExpires":null,"metered":false,"lifeTimeWarranty":false,"text1":null,"text2":null,"text3":null,"text4":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("unit", token, payload);
Util.ValidateDataReturnResponseOk(a);
long Id = a.ObjectResponse["data"]["id"].Value<long>();
Id.Should().BeGreaterThan(0);
a.ObjectResponse["data"]["serial"].Value<string>().Should().Be(serial);
// GET
a = await Util.GetAsync($"unit/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["serial"].Value<string>().Should().Be(serial);
a.ObjectResponse["data"]["customerId"].Value<long>().Should().Be(1);
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// PUT (update description)
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"active":true,"notes":null,"wiki":null,"customFields":"{}","tags":[],"serial":"{{serial}}","customerId":1,"parentUnitId":null,"unitModelId":null,"unitHasOwnAddress":false,"boughtHere":false,"purchasedFromVendorId":null,"receipt":null,"purchasedDate":null,"description":"Updated description","replacedByUnitId":null,"overrideModelWarranty":false,"warrantyLength":null,"warrantyTerms":null,"contractId":null,"contractExpires":null,"metered":false,"lifeTimeWarranty":false,"text1":null,"text2":null,"text3":null,"text4":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("unit", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"unit/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["description"].Value<string>().Should().Be("Updated description");
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("unit", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"unit/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"unit/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

75
Vendor/VendorCrud.cs vendored Normal file
View File

@@ -0,0 +1,75 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class VendorCrud
{
/// <summary>
/// Full CRUD for a Vendor, including concurrency violation and alert retrieval.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
// CREATE
var name = Util.Uniquify("Test Vendor");
var payload = $$"""
{"id":0,"concurrency":0,"name":"{{name}}","active":true,"notes":"Test vendor notes","wiki":null,"customFields":"{\"c1\":\"test\"}","tags":[],"contact":null,"contactNotes":null,"alertNotes":"Vendor alert test text","webAddress":null,"accountNumber":null,"phone1":"555-1234","phone2":null,"phone3":null,"phone4":null,"phone5":null,"emailAddress":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("vendor", 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(name);
// GET
a = await Util.GetAsync($"vendor/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(name);
a.ObjectResponse["data"]["phone1"].Value<string>().Should().Be("555-1234");
var concurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// GET ALERT
a = await Util.GetAsync($"vendor/alert/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"].Value<string>().Should().Be("Vendor alert test text");
// PUT (update name and phone)
var updatedName = Util.Uniquify("Updated Vendor");
var putPayload = $$"""
{"id":{{Id}},"concurrency":{{concurrency}},"name":"{{updatedName}}","active":true,"notes":"Test vendor notes","wiki":null,"customFields":"{\"c1\":\"test\"}","tags":[],"contact":null,"contactNotes":null,"alertNotes":"Vendor alert test text","webAddress":null,"accountNumber":null,"phone1":"555-9999","phone2":null,"phone3":null,"phone4":null,"phone5":null,"emailAddress":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("vendor", token, putPayload);
Util.ValidateHTTPStatusCode(a, 200);
var newConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newConcurrency.Should().NotBe(concurrency, "concurrency should have been bumped by the update");
// Verify the update was persisted
a = await Util.GetAsync($"vendor/{Id}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["name"].Value<string>().Should().Be(updatedName);
a.ObjectResponse["data"]["phone1"].Value<string>().Should().Be("555-9999");
// CONCURRENCY VIOLATION: PUT with stale concurrency should return 409
a = await Util.PutAsync("vendor", token, putPayload); // putPayload still has old concurrency
Util.ValidateConcurrencyError(a);
// DELETE
a = await Util.DeleteAsync($"vendor/{Id}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm deleted
a = await Util.GetAsync($"vendor/{Id}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons

View File

@@ -332,165 +332,165 @@ namespace raven_integration
/// <summary>
/// Test all CRUD routes for a WorkOrder
/// Build up graph, update graph, remove graph bit by bit
/// </summary>
[Fact]
public async Task DuplicateWorks()
{
/////////////////////////////////////////////////////////////////////////
// CREATE
//
dynamic d = new JObject();
d.active = true;
d.notes = "WOHEADER DuplicateWorks";
d.wiki = "DuplicateWorks integration test";
// /// <summary>
// /// Test all CRUD routes for a WorkOrder
// /// Build up graph, update graph, remove graph bit by bit
// /// </summary>
// [Fact]
// public async Task DuplicateWorks()
// {
// /////////////////////////////////////////////////////////////////////////
// // CREATE
// //
// dynamic d = new JObject();
// d.active = true;
// d.notes = "WOHEADER DuplicateWorks";
// d.wiki = "DuplicateWorks integration test";
ApiResponse a = await Util.PostAsync("workorder", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
dynamic WorkorderToUpdate = a.ObjectResponse["data"];
long WorkOrderId = a.ObjectResponse["data"]["id"].Value<long>();
// ApiResponse a = await Util.PostAsync("workorder", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// dynamic WorkorderToUpdate = a.ObjectResponse["data"];
// long WorkOrderId = a.ObjectResponse["data"]["id"].Value<long>();
#region CREATE first woitem
// #region CREATE first woitem
//CREATE WOITEM 1
d = new JObject();
d.active = true;
d.notes = "woitem DuplicateWorks";
d.wiki = "# woitem test wiki";
d.workOrderId = WorkOrderId;
// //CREATE WOITEM 1
// d = new JObject();
// d.active = true;
// d.notes = "woitem DuplicateWorks";
// d.wiki = "# woitem test wiki";
// d.workOrderId = WorkOrderId;
a = await Util.PostAsync("workorder/items", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
dynamic WorkOrderItemToUpdate = a.ObjectResponse["data"];
long WorkOrderItem1Id = a.ObjectResponse["data"]["id"].Value<long>();
// a = await Util.PostAsync("workorder/items", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// dynamic WorkOrderItemToUpdate = a.ObjectResponse["data"];
// long WorkOrderItem1Id = a.ObjectResponse["data"]["id"].Value<long>();
//CREATE WOITEMPART
d = new JObject();
d.active = true;
d.notes = "woitempart DuplicateWorks";
// d.wiki = "# woitempart test wiki";
d.workOrderItemId = WorkOrderItem1Id;
// //CREATE WOITEMPART
// d = new JObject();
// d.active = true;
// d.notes = "woitempart DuplicateWorks";
// // d.wiki = "# woitempart test wiki";
// d.workOrderItemId = WorkOrderItem1Id;
a = await Util.PostAsync("workorder/items/parts", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
dynamic dPartToUpdate = a.ObjectResponse["data"];
long PartId = (long)a.ObjectResponse["data"]["id"];
// a = await Util.PostAsync("workorder/items/parts", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// dynamic dPartToUpdate = a.ObjectResponse["data"];
// long PartId = (long)a.ObjectResponse["data"]["id"];
//CREATE TWO WOITEMLABORS
//ONE
d = new JObject();
d.active = true;
d.notes = "woitemlabor DuplicateWorks";
// d.wiki = "# woitemlabor test wiki";
d.workOrderItemId = WorkOrderItem1Id;
// //CREATE TWO WOITEMLABORS
// //ONE
// d = new JObject();
// d.active = true;
// d.notes = "woitemlabor DuplicateWorks";
// // d.wiki = "# woitemlabor test wiki";
// d.workOrderItemId = WorkOrderItem1Id;
a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
dynamic dLaborOne = a.ObjectResponse["data"];
long Labor1Id = a.ObjectResponse["data"]["id"].Value<long>();
// a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// dynamic dLaborOne = a.ObjectResponse["data"];
// long Labor1Id = a.ObjectResponse["data"]["id"].Value<long>();
//TWO
d = new JObject();
d.active = true;
d.notes = "woitemlabor2 DuplicateWorks";
// d.wiki = "# woitemlabor2 test wiki";
d.workOrderItemId = WorkOrderItem1Id;
// //TWO
// d = new JObject();
// d.active = true;
// d.notes = "woitemlabor2 DuplicateWorks";
// // d.wiki = "# woitemlabor2 test wiki";
// d.workOrderItemId = WorkOrderItem1Id;
a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
long WorkOrderItemLaborId2 = a.ObjectResponse["data"]["id"].Value<long>();
// a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// long WorkOrderItemLaborId2 = a.ObjectResponse["data"]["id"].Value<long>();
#endregion create first woitem
// #endregion create first woitem
#region CREATE Second woitem
/////////////////////////////////////////
// Second woitem
//
//CREATE WOITEM-2
d = new JObject();
d.active = true;
d.notes = "woitem-2 DuplicateWorks";
d.wiki = "# woitem test wiki";
d.workOrderId = WorkOrderId;
// #region CREATE Second woitem
// /////////////////////////////////////////
// // Second woitem
// //
// //CREATE WOITEM-2
// d = new JObject();
// d.active = true;
// d.notes = "woitem-2 DuplicateWorks";
// d.wiki = "# woitem test wiki";
// d.workOrderId = WorkOrderId;
a = await Util.PostAsync("workorder/items", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
long WorkOrderItem2Id = a.ObjectResponse["data"]["id"].Value<long>();
// a = await Util.PostAsync("workorder/items", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// long WorkOrderItem2Id = a.ObjectResponse["data"]["id"].Value<long>();
//CREATE WOITEMPART-2
d = new JObject();
d.active = true;
d.notes = "woitem-2-partDuplicateWorks";
// d.wiki = "# woitempart test wiki";
d.workOrderItemId = WorkOrderItem2Id;
// //CREATE WOITEMPART-2
// d = new JObject();
// d.active = true;
// d.notes = "woitem-2-partDuplicateWorks";
// // d.wiki = "# woitempart test wiki";
// d.workOrderItemId = WorkOrderItem2Id;
a = await Util.PostAsync("workorder/items/parts", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
// a = await Util.PostAsync("workorder/items/parts", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
//CREATE WOITEMLABOR(S)-2
//ONE-2
d = new JObject();
d.active = true;
d.notes = "woitem-2-laborDuplicateWorks";
//d.wiki = "# woitemlabor test wiki";
d.workOrderItemId = WorkOrderItem2Id;
a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
// //CREATE WOITEMLABOR(S)-2
// //ONE-2
// d = new JObject();
// d.active = true;
// d.notes = "woitem-2-laborDuplicateWorks";
// //d.wiki = "# woitemlabor test wiki";
// d.workOrderItemId = WorkOrderItem2Id;
// a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
//TWO-2
d = new JObject();
d.active = true;
d.notes = "woitem-2-labor2DuplicateWorks";
//d.wiki = "# woitemlabor2 test wiki";
d.workOrderItemId = WorkOrderItem2Id;
a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
// //TWO-2
// d = new JObject();
// d.active = true;
// d.notes = "woitem-2-labor2DuplicateWorks";
// //d.wiki = "# woitemlabor2 test wiki";
// d.workOrderItemId = WorkOrderItem2Id;
// a = await Util.PostAsync("workorder/items/labors", await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
#endregion create second woitem
// #endregion create second woitem
#region Duplicate and Validate
// #region Duplicate and Validate
//DUPLICATE - should make a copy of above, save and return entire object graph just like a get operation
a = await Util.PostAsync("workorder/duplicate/" + WorkOrderId.ToString(), await Util.GetTokenAsync("BizAdmin"), d.ToString());
Util.ValidateDataReturnResponseOk(a);
long DuplicateWorkOrderId = a.ObjectResponse["data"]["id"].Value<long>();
DuplicateWorkOrderId.Should().NotBe(WorkOrderId);
((long)a.ObjectResponse["data"]["serial"]).Should().NotBe(0);
// //DUPLICATE - should make a copy of above, save and return entire object graph just like a get operation
// a = await Util.PostAsync("workorder/duplicate/" + WorkOrderId.ToString(), await Util.GetTokenAsync("BizAdmin"), d.ToString());
// Util.ValidateDataReturnResponseOk(a);
// long DuplicateWorkOrderId = a.ObjectResponse["data"]["id"].Value<long>();
// DuplicateWorkOrderId.Should().NotBe(WorkOrderId);
// ((long)a.ObjectResponse["data"]["serial"]).Should().NotBe(0);
// //RETRIEVE WORKORDER AND VALIDATE ENTIRE
// a = await Util.GetAsync("workorder/" + WorkOrderId.ToString(), await Util.GetTokenAsync("BizAdmin"));
// Util.ValidateDataReturnResponseOk(a);
// // //RETRIEVE WORKORDER AND VALIDATE ENTIRE
// // a = await Util.GetAsync("workorder/" + WorkOrderId.ToString(), await Util.GetTokenAsync("BizAdmin"));
// // Util.ValidateDataReturnResponseOk(a);
var w = a.ObjectResponse["data"];
w["items"].Count().Should().Be(2);
((string)w["notes"]).Should().Be("WOHEADER DuplicateWorks");
// var w = a.ObjectResponse["data"];
// w["items"].Count().Should().Be(2);
// ((string)w["notes"]).Should().Be("WOHEADER DuplicateWorks");
var woitem1 = w["items"].FirstOrDefault(z => (long)z["id"] == WorkOrderItem1Id);
woitem1["labors"].Count().Should().Be(2);
woitem1["parts"].Count().Should().Be(1);
((string)woitem1["notes"]).Should().Be("woitem DuplicateWorks");
((string)woitem1["labors"].FirstOrDefault(z => (long)z["id"] == Labor1Id)["notes"]).Should().Be("woitemlabor DuplicateWorks");
((string)woitem1["parts"].FirstOrDefault(z => (long)z["id"] == PartId)["notes"]).Should().Be("woitempart DuplicateWorks");
// var woitem1 = w["items"].FirstOrDefault(z => (long)z["id"] == WorkOrderItem1Id);
// woitem1["labors"].Count().Should().Be(2);
// woitem1["parts"].Count().Should().Be(1);
// ((string)woitem1["notes"]).Should().Be("woitem DuplicateWorks");
// ((string)woitem1["labors"].FirstOrDefault(z => (long)z["id"] == Labor1Id)["notes"]).Should().Be("woitemlabor DuplicateWorks");
// ((string)woitem1["parts"].FirstOrDefault(z => (long)z["id"] == PartId)["notes"]).Should().Be("woitempart DuplicateWorks");
var woitem2 = w["items"].FirstOrDefault(z => (long)z["id"] == WorkOrderItem2Id);
woitem2["labors"].Count().Should().Be(2);
woitem2["parts"].Count().Should().Be(1);
((string)woitem2["notes"]).Should().Be("woitem-2 DuplicateWorks");
woitem2["labors"].FirstOrDefault(z => (string)z["notes"] == "woitem-2-labor2DuplicateWorks").Should().NotBeNull();
woitem2["parts"].FirstOrDefault(z => (string)z["notes"] == "woitem-2-partDuplicateWorks").Should().NotBeNull();
// var woitem2 = w["items"].FirstOrDefault(z => (long)z["id"] == WorkOrderItem2Id);
// woitem2["labors"].Count().Should().Be(2);
// woitem2["parts"].Count().Should().Be(1);
// ((string)woitem2["notes"]).Should().Be("woitem-2 DuplicateWorks");
// woitem2["labors"].FirstOrDefault(z => (string)z["notes"] == "woitem-2-labor2DuplicateWorks").Should().NotBeNull();
// woitem2["parts"].FirstOrDefault(z => (string)z["notes"] == "woitem-2-partDuplicateWorks").Should().NotBeNull();
#endregion validate entire
// #endregion validate entire
}
// }

106
claude.md
View File

@@ -2,32 +2,98 @@
## What This Is
Old integration test project for the AyaNova v8 API. These tests were written during initial API development and have not been maintained. They target .NET Core 3.x and need evaluation before any further use.
Integration test project for the AyaNova v8 API. Tests target .NET 8 and run against a locally running server (`http://localhost:7575`).
## Current Goal
Run with:
Triage these tests to determine what is salvageable:
```
dotnet test raven-integration.csproj
```
1. Get the project compiling against .NET 8 (update TargetFramework, NuGet packages)
2. Inventory all test files and what they cover
3. Identify tests that still match current API endpoints and behavior
4. Decide per-test: **fix it**, **rewrite it**, or **delete it**
5. Broken tests that aren't worth fixing should be deleted — broken tests are worse than no tests
## Current Status (March 2026)
## The Current Backend
The initial triage and rebuild is complete. The project now compiles against net8.0, old broken tests have been removed or rewritten, and new gap-coverage tests have been added. All tests pass except three that are blocked by known server-side bugs:
The production API these tests should validate lives at `../raven/server/AyaNova/` and runs ASP.NET Core 8 with PostgreSQL. Compare test expectations against the actual current API, not what the API looked like when these tests were written.
- **CustomerWithWorkorders_cannotbedeleted** — server returns 500 instead of 400 (case 4653)
- **ContractCrud.CRUD** — minor server-side issue (tracked separately)
- **DirectMessage_SendAndDelete** - server issue prevents delivery as considered a "duplicate" incorrectly. (case 4654)
## Known Issues
These are intentionally left failing until server-side fixes are applied as part of the refactor phase. Except the directMessage test is set to skip as it has a 65 second timeout that would slow down testing unnecessarily.
- `dotnet test` fails because multiple .csproj/.sln files exist — specify the correct one
- Project targets outdated .NET Core 3.x — needs TargetFramework update to net8.0
- NuGet package references are likely outdated and may need updating
- Test data setup and API endpoints may have changed significantly
## What Is Covered
## What Success Looks Like
### Tier 1 (Critical) — Complete
- A clear inventory of what tests exist and what they cover
- Any salvageable tests updated to compile and run against .NET 8
- Dead or irrelevant tests removed
- A gap analysis: what critical API paths have no test coverage
- **DataList** — `DataList/DataListOperations.cs`
- Saved filter CRUD (create, list, update visibility, delete, confirm deleted)
- Default filter creation blocked by API
- String filtering: contains, starts-with, equals, not-equal
- Date filtering: range (GT/LT), relative keywords (*yesterday*, *tomorrow*, *thisyear*, *NULL*)
- Multi-column AND filter
- Sort ascending and descending on date column
- Pagination (offset/limit, totalRecordCount consistency, non-overlapping pages)
- Column view CRUD (custom column order round-trip)
- Rights enforcement (user with no roles gets 403)
- **Quote** — `Quote/QuoteCrud.cs`
- Header CRUD + concurrency violation + id-from-number lookup
- QuoteItem CRUD
- QuoteItemLabor sub-type CRUD
- **Customer** — `Customer/CustomerCrud.cs`
- CRUD + concurrency violation + alert retrieval
- Referential integrity: customer with linked work order cannot be deleted (test exists; server bug pending fix)
### Tier 2 (Important) — Complete
- **Contract** — `Contract/ContractCrud.cs` — CRUD + concurrency
- **PM** — `PM/PMCrud.cs` — header CRUD + PMItem + concurrency + id-from-number
- **Authorization** — `Authentication/AuthRights.cs` — unauthenticated 401, unauthorized 403
- **Schedule** — `Schedule/ScheduleReads.cs` — service schedule + personal schedule reads
- **Part** — `Part/PartCrud.cs` — CRUD + concurrency + serial numbers round-trip
### Tier 3 (Reference Data / Nice to Have) — Complete
- **Vendor** — `Vendor/VendorCrud.cs` — CRUD + concurrency + alert
- **ServiceRate / TravelRate / TaxCode** — `Reference/ReferenceCrud.cs` — CRUD + concurrency
- **Unit / UnitModel** — `Unit/UnitCrud.cs` — CRUD for both types
- **Memo** — `Memo/MemoCrud.cs` — create + read + delete
- **Notification** — `Notification/NotificationOps.cs` — count, list, send, delete
### Already Covered (from original tests)
- User CRUD, concurrency, password change, inactive user login
- Project CRUD and concurrency
- WorkOrder + nested items/parts/labor/units CRUD
- Attachments (upload/download/delete/authorization)
- Search (phrase, wildcard, tags, serial, deletion cleanup)
- Custom forms
- Pick lists
- Translations
- Event log (object log, user log, pagination)
- Tag bulk operations
- Server health, metrics, log files
- Global biz settings (fetch + round-trip PUT)
## What Is Intentionally Not Covered
- **Report generation** — async job pattern with polling; deferred
- **Stock level updates** — require seeded warehouse IDs not known at test time
- **Auth roles deep test** — role-change behavioral tests; the existing AuthRights.cs covers the critical 401/403 paths
- **EnumList, Name lookup** — low-risk reference reads; not needed for refactor protection
## Known Configuration
- API base: `http://localhost:7575/api/v8/`
- Seeded logins: `superuser` (password: `l3tm3in`), `BizAdmin`, `OpsAdmin`, `SubContractorRestricted`
- Seeded customer id=1 is always safe to reference
- Time zone adjustment for date filters: -7 (see `util.cs` `TIME_ZONE_ADJUSTMENT`)
- DataList class names ARE the list keys (e.g., `WorkOrderDataList`, `CustomerDataList`)
## What Comes Next (Refactor Phase)
This test suite exists to protect against regressions during the Step D refactor. Before starting the refactor:
1. Confirm all tests pass (except the three known server bugs above)
2. Fix the three server-side bugs (case 4653, case 4654 and contract issue)
3. Then proceed with the Step D refactor — tests will catch regressions

52
memory/MEMORY.md Normal file
View File

@@ -0,0 +1,52 @@
# raven-test-integration memory
## Project purpose
Integration test project for AyaNova v8 API. Target: net8.0. Run with:
dotnet test raven-integration.csproj
## Key constants / config
- API base: http://localhost:7575/api/v8/ (util.cs line 31)
- Seeded logins: superuser (password: l3tm3in), BizAdmin, SubContractorRestricted
- Seeded customer id=1 is always safe to reference in payloads
- Time zone adjustment for date filters: -7 (util.cs TIME_ZONE_ADJUSTMENT)
## Known good enum values used in tests
- ContractOverrideType: PriceDiscount=1, CostMarkup=2
- PMTimeUnit: Minutes=2, Hours=3, Days=4, Months=6, Years=7
- AyaDaysOfWeek (flags): Mon=1,Tue=2,Wed=4,Thu=8,Fri=16,Sat=32,Sun=64
— 96 = Saturday|Sunday (used in PM tests)
## DataList key names
Class names in AyaNova.DataList namespace ARE the list keys.
Examples: WorkOrderDataList, CustomerDataList, QuoteDataList, PMDataList, ContractDataList
## API response conventions
- POST: 201, {data: full object}
- GET: 200, {data: full object}
- DELETE: 204, no body
- Customer/QuoteItem/PMItem PUT: 200, {data: {Concurrency: N}} (just concurrency)
- Quote/PM/Contract/WorkOrder header PUT: 200, {data: full object}
- Concurrency conflict: 409
- Referential integrity violation: 400, error.code = 2200
- Not found: 404, error.code = 2010
## Files written (Feb 2026 session)
- Customer/CustomerCrud.cs — CRUD + concurrency + alert + referential integrity (2 tests)
- Quote/QuoteCrud.cs — header CRUD + QuoteItem + QuoteItemLabor + concurrency + id-from-number
- Contract/ContractCrud.cs — CRUD + concurrency (contract PUT returns full object)
- PM/PMCrud.cs — header CRUD + PMItem + concurrency + id-from-number
- DataList/DataListOperations.cs — SavedFilterCRUD (live) + filter/sort stubs (skipped)
## DataList saved filter route
POST/GET/PUT/DELETE: /data-list-filter (NOT data-list-view — that was old)
List endpoint: GET /data-list-filter/list?ListKey=WorkOrderDataList
userId=0 in payload is fine; server assigns from auth token
## Todo.md coverage status
Tier 1: DataList saved filter CRUD done; filter/sort stubs need browser payload capture
Tier 1: Quote CRUD done
Tier 1: Customer CRUD done
Tier 2: Contract CRUD done
Tier 2: PM CRUD done
Tier 2: Auth roles, Schedule, Part — not yet started
Tier 3: All not yet started