mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 12:31:59 +00:00
180 lines
8.3 KiB
C#
180 lines
8.3 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using ResolveSpec;
|
|
using Xunit;
|
|
|
|
public class Stub : HttpMessageHandler
|
|
{
|
|
public HttpRequestMessage? Request;
|
|
public string Body = "";
|
|
readonly HttpStatusCode _status;
|
|
readonly string _json;
|
|
readonly Dictionary<string, string> _headers;
|
|
|
|
public Stub(HttpStatusCode status, string json, Dictionary<string, string>? headers = null)
|
|
{
|
|
_status = status; _json = json; _headers = headers ?? new();
|
|
}
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
|
{
|
|
Request = request;
|
|
Body = request.Content == null ? "" : await request.Content.ReadAsStringAsync(ct);
|
|
var r = new HttpResponseMessage(_status) { Content = new StringContent(_json, Encoding.UTF8, "application/json") };
|
|
foreach (var (k, v) in _headers)
|
|
if (!r.Headers.TryAddWithoutValidation(k, v)) r.Content.Headers.TryAddWithoutValidation(k, v);
|
|
return r;
|
|
}
|
|
}
|
|
|
|
public class ResolveSpecTests
|
|
{
|
|
static (ResolveSpecClient, Stub) Make(HttpStatusCode s, string json)
|
|
{
|
|
var stub = new Stub(s, json);
|
|
var o = new ClientOptions { Token = "tok", HttpClient = new HttpClient(stub) };
|
|
o.Headers["X-Tenant"] = "a";
|
|
return (new ResolveSpecClient("http://localhost:3000/", o), stub);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadPostsBody()
|
|
{
|
|
var (c, s) = Make(HttpStatusCode.OK, """{"success":true,"data":[{"id":1}]}""");
|
|
var r = await c.ReadAsync("public", "users", null, new Options { Limit = 5, Filters = new() { new FilterOption { Column = "a", Operator = "eq", Value = 1 } } });
|
|
Assert.Equal(HttpMethod.Post, s.Request!.Method);
|
|
Assert.Equal("/public/users", s.Request.RequestUri!.AbsolutePath);
|
|
Assert.Equal("Bearer tok", s.Request.Headers.Authorization!.ToString());
|
|
Assert.Equal("a", s.Request.Headers.GetValues("X-Tenant").Single());
|
|
using var body = JsonDocument.Parse(s.Body);
|
|
Assert.Equal("read", body.RootElement.GetProperty("operation").GetString());
|
|
Assert.Equal(5, body.RootElement.GetProperty("options").GetProperty("limit").GetInt32());
|
|
Assert.False(body.RootElement.TryGetProperty("id", out _));
|
|
Assert.Single(r.Decode<List<Dictionary<string, int>>>()!);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IdPlacement()
|
|
{
|
|
var (c, s) = Make(HttpStatusCode.OK, """{"success":true,"data":{}}""");
|
|
await c.ReadAsync("s", "e", 7);
|
|
Assert.Equal("/s/e/7", s.Request!.RequestUri!.AbsolutePath);
|
|
await c.UpdateAsync("s", "e", new { a = 1 }, new[] { "1", "2" });
|
|
Assert.Equal("/s/e", s.Request!.RequestUri!.AbsolutePath);
|
|
using (var b = JsonDocument.Parse(s.Body))
|
|
{
|
|
Assert.Equal(2, b.RootElement.GetProperty("id").GetArrayLength());
|
|
Assert.Equal("update", b.RootElement.GetProperty("operation").GetString());
|
|
}
|
|
await c.DeleteAsync("s", "e", "a/b");
|
|
Assert.Equal("/s/e/a%2Fb", s.Request!.RequestUri!.AbsoluteUri[(s.Request.RequestUri.AbsoluteUri.IndexOf("/s/e", StringComparison.Ordinal))..]);
|
|
Assert.Contains("\"delete\"", s.Body);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Errors()
|
|
{
|
|
var (c, _) = Make(HttpStatusCode.BadRequest, """{"success":false,"error":{"code":"x","message":"bad","detail":"why"}}""");
|
|
var e = await Assert.ThrowsAsync<ResolveSpecException>(() => c.ReadAsync("s", "e"));
|
|
Assert.Equal((400, "x", "bad", "why"), (e.StatusCode, e.Error.Code, e.Message, e.Error.Detail));
|
|
|
|
var (c2, _) = Make(HttpStatusCode.BadGateway, "bad gateway");
|
|
var e2 = await Assert.ThrowsAsync<ResolveSpecException>(() => c2.ReadAsync("s", "e"));
|
|
Assert.Equal((502, "bad gateway"), (e2.StatusCode, e2.Message));
|
|
|
|
var (c3, _) = Make(HttpStatusCode.OK, """{"success":false,"error":{"code":"c","message":"nope"}}""");
|
|
var e3 = await Assert.ThrowsAsync<ResolveSpecException>(() => c3.ReadAsync("s", "e"));
|
|
Assert.Equal("nope", e3.Message);
|
|
}
|
|
}
|
|
|
|
public class FuncSpecTests
|
|
{
|
|
[Fact]
|
|
public void HeaderFilters()
|
|
{
|
|
var h = FuncSpecClient.BuildHeaders(new FuncSpecOptions
|
|
{
|
|
Filters = new()
|
|
{
|
|
new() { Column = "status", Operator = "eq", Value = "active" },
|
|
new() { Column = "age", Operator = "gte", Value = 18 },
|
|
new() { Column = "name", Operator = "contains", Value = "x", LogicOperator = "OR" },
|
|
new() { Column = "deleted", Operator = "is_null" },
|
|
new() { Column = "id", Operator = "in", Value = new[] { 1, 2 } },
|
|
new() { Column = "p", Operator = "between_inclusive", Value = new[] { 1, 5 } },
|
|
},
|
|
});
|
|
Assert.Equal(new Dictionary<string, string>
|
|
{
|
|
["X-FieldFilter-status"] = "active",
|
|
["X-SearchOp-greaterthanorequal-age"] = "18",
|
|
["X-SearchOr-contains-name"] = "x",
|
|
["X-SearchOp-empty-deleted"] = "",
|
|
["X-SearchOp-in-id"] = "1,2",
|
|
["X-SearchOp-betweeninclusive-p"] = "1,5",
|
|
}, h);
|
|
}
|
|
|
|
[Fact]
|
|
public void HeaderMiscAndEncoding()
|
|
{
|
|
var h = FuncSpecClient.BuildHeaders(new FuncSpecOptions
|
|
{
|
|
SearchFilters = new() { ["name"] = "bob" }, CustomSqlWhere = "a = 1", CustomSqlOr = "b = 2",
|
|
Sort = new() { new() { Column = "name", Direction = "asc" }, new() { Column = "created_at", Direction = "DESC" } },
|
|
Limit = 5, Offset = 10, Distinct = true, SkipCount = true, SkipCache = false, ResponseFormat = "syncfusion",
|
|
});
|
|
Assert.Equal("name ASC,created_at DESC", h["X-Sort"]);
|
|
Assert.Equal("bob", h["X-SearchFilter-name"]);
|
|
Assert.Equal("a = 1", h["X-Custom-SQL-W"]);
|
|
Assert.Equal("false", h["X-SkipCache"]);
|
|
Assert.Equal("true", h["X-Syncfusion"]);
|
|
|
|
h = FuncSpecClient.BuildHeaders(new FuncSpecOptions { Filters = new()
|
|
{
|
|
new() { Column = "n", Operator = "eq", Value = "héllo" },
|
|
new() { Column = "m", Operator = "eq", Value = " pad" },
|
|
} });
|
|
Assert.StartsWith("ZIP_", h["X-FieldFilter-n"]);
|
|
Assert.Equal("héllo", FuncSpecClient.DecodeHeaderValue(h["X-FieldFilter-n"]));
|
|
Assert.Equal(" pad", FuncSpecClient.DecodeHeaderValue(h["X-FieldFilter-m"]));
|
|
}
|
|
|
|
[Fact]
|
|
public void QueryBuilding()
|
|
{
|
|
var q = FuncSpecClient.BuildQuery(new Dictionary<string, object?> { ["a"] = true, ["b"] = new[] { "x", "y" }, ["c"] = null, ["d"] = 3 });
|
|
Assert.Equal(new[] { "a=true", "b=x", "b=y", "d=3" }, q.Select(kv => $"{kv.Key}={kv.Value}"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueryListMetadata()
|
|
{
|
|
var stub = new Stub((HttpStatusCode)206, """[{"id":1},{"id":2}]""", new() { ["Content-Range"] = "items 10-12/50" });
|
|
var c = new FuncSpecClient("http://x", new ClientOptions { Token = "tok", HttpClient = new HttpClient(stub) });
|
|
var r = await c.QueryListAsync("/api/users", new Dictionary<string, object?> { ["org"] = 1 }, new FuncSpecOptions { Limit = 2 });
|
|
Assert.Equal("GET", stub.Request!.Method.Method);
|
|
Assert.Equal("/api/users", stub.Request.RequestUri!.AbsolutePath);
|
|
Assert.Equal("?org=1", stub.Request.RequestUri.Query);
|
|
Assert.Equal("2", stub.Request.Headers.GetValues("X-Limit").Single());
|
|
Assert.Equal((50L, 2L, 50L, 2L, 10L), (r.Metadata!.Total, r.Metadata.Count, r.Metadata.Filtered, r.Metadata.Limit, r.Metadata.Offset));
|
|
Assert.Equal(2, r.Data.GetArrayLength());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QuerySingleAndError()
|
|
{
|
|
var ok = new FuncSpecClient("http://x", new ClientOptions { HttpClient = new HttpClient(new Stub(HttpStatusCode.OK, """{"id":1}""")) });
|
|
var r = await ok.QueryAsync("api/u");
|
|
Assert.Null(r.Metadata);
|
|
Assert.Equal(1, r.Data.GetProperty("id").GetInt32());
|
|
|
|
var bad = new FuncSpecClient("http://x", new ClientOptions { HttpClient = new HttpClient(new Stub(HttpStatusCode.BadRequest,
|
|
"""{"success":false,"error":{"code":"hook_error","message":"Hook execution failed","detail":"authentication required"}}""")) });
|
|
var e = await Assert.ThrowsAsync<ResolveSpecException>(() => bad.QueryAsync("api/u"));
|
|
Assert.Equal(("hook_error", "authentication required"), (e.Error.Code, e.Error.Detail));
|
|
}
|
|
}
|