using Invercargill; using Invercargill.DataStructures; using Invercargill.Mapping; using InvercargillJson; using Inversion; using Statum; namespace Statum.Tests { int failures = 0; int passes = 0; void check(bool condition, string label) { if (condition) { passes++; print("PASS %s\n", label); } else { failures++; print("FAIL %s\n", label); } } /** * Scalar model in the style of Percipio's login private data: the field * types that previously crashed (int64, bool) or mis-serialised (int) the * encryption path. */ public class LoginPrivate : Object { public int64 user_id { get; set; } public bool is_admin { get; set; } public int attempts { get; set; } public double score { get; set; } public string username { get; set; } public long session_no { get; set; } } /** List-bearing model: a `string[]` plus a collection of a registered type. */ public class ArrayModel : Object { public string[] tags { get; set; } public Series refs { get; set; } } /** Typed action WITHOUT a `private_type` override — demonstrates S-8 erosion. */ public class ErodingAction : TypedStatumAction { public Type exposed_private_type() { return private_type; } public LoginPrivate? exposed_request_private() { return request_private; } public void feed(Properties props) { request = new StatumRequest() { action_private = props }; } public override async DirectiveBuilder handle() throws GLib.Error { return directives(); } } /** Typed action WITH the `private_type` override — the supported pattern. */ public class FixedAction : TypedStatumAction { protected override Type private_type { get { return typeof(LoginPrivate); } } public Type exposed_private_type() { return private_type; } public LoginPrivate? exposed_request_private() { return request_private; } public void feed(Properties props) { request = new StatumRequest() { action_private = props }; } public override async DirectiveBuilder handle() throws GLib.Error { return directives(); } } LoginPrivate sample_private() { return new LoginPrivate() { user_id = (int64)9007199254740993, is_admin = true, attempts = 3, score = 0.5, username = "bbarrow", session_no = (long)123456789012345 }; } void assert_private_matches(LoginPrivate expected, LoginPrivate actual, string label) { check(actual.user_id == expected.user_id, label + ": user_id int64 round-trip"); check(actual.is_admin == expected.is_admin, label + ": is_admin bool round-trip"); check(actual.attempts == expected.attempts, label + ": attempts int round-trip"); check(actual.score == expected.score, label + ": score double round-trip"); check(actual.username == expected.username, label + ": username string round-trip"); check(actual.session_no == expected.session_no, label + ": session_no long round-trip"); } void test_scalar_public_path() throws GLib.Error { var model = sample_private(); var props = GObjectMapping.to_properties(model); var wire = new JsonElement.from_properties(props).stringify(); check(wire.contains("\"user_id\":9007199254740993"), "public path: int64 serialises as an unquoted JSON number"); check(wire.contains("\"is_admin\":true"), "public path: bool serialises as an unquoted JSON boolean"); check(wire.contains("\"attempts\":3"), "public path: int serialises as an unquoted JSON number, not a string"); check(wire.contains("\"score\":0.5"), "public path: double serialises as an unquoted JSON number"); var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate), new JsonElement.from_string(wire).as()); assert_private_matches(model, back, "public path"); } void test_scalar_encryption_path(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error { var model = sample_private(); var blob = encryption.author_properties("statum-tests", GObjectMapping.to_properties(model)); var props = encryption.read_properties("statum-tests", blob); var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate), props); assert_private_matches(model, back, "encryption path"); } void test_arrays(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error { var first = new Model.ActionDto() { uri = "/_statum/action/aaaa", method = "POST" }; var second = new Model.ActionDto() { uri = "/_statum/action/bbbb", method = "DELETE" }; var refs = new Series(); refs.add(first); refs.add(second); var model = new ArrayModel() { tags = new string[] { "alpha", "beta", "gamma" }, refs = refs }; var props = GObjectMapping.to_properties(model); var wire = new JsonElement.from_properties(props).stringify(); check(wire.contains("\"tags\":[\"alpha\",\"beta\",\"gamma\"]"), "arrays: string[] serialises as a JSON array"); check(wire.contains("\"uri\":\"/_statum/action/bbbb\"") && wire.contains("\"method\":\"DELETE\""), "arrays: registered-Object collection serialises as an array of nested objects"); var parsed = new JsonElement.from_string(wire).as(); var array_model = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel), parsed); check(array_model.tags.length == 3 && array_model.tags[0] == "alpha" && array_model.tags[1] == "beta" && array_model.tags[2] == "gamma", "arrays: string[] read-back from JSON array"); var blob = encryption.author_properties("statum-tests", props); var decrypted = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel), encryption.read_properties("statum-tests", blob)); check(decrypted.tags.length == 3 && decrypted.tags[2] == "gamma", "arrays: string[] round-trips through the encryption path"); } void test_typed_action_private(Inversion.Scope scope, Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error { var eroding = (ErodingAction) scope.resolve_type(typeof(ErodingAction)); check(eroding.exposed_private_type() == typeof(void), "S-8 repro: typeof(TPrivate) erodes to void for a reflectively-instantiated action"); eroding.feed(new PropertyDictionary()); check(eroding.exposed_request_private() == null, "S-8 repro: un-overridden request_private yields null without crashing"); var fixed_action = (FixedAction) scope.resolve_type(typeof(FixedAction)); check(fixed_action.exposed_private_type() == typeof(LoginPrivate), "S-8 fix: private_type override captures the GType from a foreign namespace"); var model = sample_private(); var blob = encryption.author_properties(fixed_action.get_type().name(), GObjectMapping.to_properties(model)); fixed_action.feed(encryption.read_properties(fixed_action.get_type().name(), blob)); var private_data = fixed_action.exposed_request_private(); check(private_data != null, "S-8 fix: request_private materialises from a sealed blob"); if (private_data != null) { assert_private_matches(model, (!)private_data, "S-8 fix"); } } void main() { GObjectMapping.register_mapper(Model.ActionDto.get_mapper()); var container = new Container(); container.register_singleton(); container.register_singleton(); container.register_singleton(); container.register_singleton(); container.register_singleton().as(); container.register_singleton(); container.register_singleton(); container.register_scoped(); container.register_scoped(); var scope = container.create_transient_scope(); var encryption = (Statum.Cryptography.EncryptionProvider) scope.resolve_type(typeof(Statum.Cryptography.EncryptionProvider)); try { test_scalar_public_path(); } catch (GLib.Error e) { failures++; print("FAIL scalar public path threw: %s\n", e.message); } try { test_scalar_encryption_path(encryption); } catch (GLib.Error e) { failures++; print("FAIL scalar encryption path threw: %s\n", e.message); } try { test_arrays(encryption); } catch (GLib.Error e) { failures++; print("FAIL arrays threw: %s\n", e.message); } try { test_typed_action_private(scope, encryption); } catch (GLib.Error e) { failures++; print("FAIL typed action private threw: %s\n", e.message); } print("---- %d passed, %d failed ----\n", passes, failures); if (failures > 0) { Process.exit(1); } } }