TestMain.vala 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Mapping;
  4. using InvercargillJson;
  5. using Inversion;
  6. using Statum;
  7. namespace Statum.Tests {
  8. int failures = 0;
  9. int passes = 0;
  10. void check(bool condition, string label) {
  11. if (condition) {
  12. passes++;
  13. print("PASS %s\n", label);
  14. } else {
  15. failures++;
  16. print("FAIL %s\n", label);
  17. }
  18. }
  19. /**
  20. * Scalar model in the style of Percipio's login private data: the field
  21. * types that previously crashed (int64, bool) or mis-serialised (int) the
  22. * encryption path.
  23. */
  24. public class LoginPrivate : Object {
  25. public int64 user_id { get; set; }
  26. public bool is_admin { get; set; }
  27. public int attempts { get; set; }
  28. public double score { get; set; }
  29. public string username { get; set; }
  30. public long session_no { get; set; }
  31. }
  32. /** List-bearing model: a `string[]` plus a collection of a registered type. */
  33. public class ArrayModel : Object {
  34. public string[] tags { get; set; }
  35. public Series<Model.ActionDto> refs { get; set; }
  36. }
  37. /** Typed action WITHOUT a `private_type` override — demonstrates S-8 erosion. */
  38. public class ErodingAction : TypedStatumAction<LoginPrivate> {
  39. public Type exposed_private_type() {
  40. return private_type;
  41. }
  42. public LoginPrivate? exposed_request_private() {
  43. return request_private;
  44. }
  45. public void feed(Properties props) {
  46. request = new StatumRequest() { action_private = props };
  47. }
  48. public override async DirectiveBuilder handle() throws GLib.Error {
  49. return directives();
  50. }
  51. }
  52. /** Typed action WITH the `private_type` override — the supported pattern. */
  53. public class FixedAction : TypedStatumAction<LoginPrivate> {
  54. protected override Type private_type { get { return typeof(LoginPrivate); } }
  55. public Type exposed_private_type() {
  56. return private_type;
  57. }
  58. public LoginPrivate? exposed_request_private() {
  59. return request_private;
  60. }
  61. public void feed(Properties props) {
  62. request = new StatumRequest() { action_private = props };
  63. }
  64. public override async DirectiveBuilder handle() throws GLib.Error {
  65. return directives();
  66. }
  67. }
  68. LoginPrivate sample_private() {
  69. return new LoginPrivate() {
  70. user_id = (int64)9007199254740993,
  71. is_admin = true,
  72. attempts = 3,
  73. score = 0.5,
  74. username = "bbarrow",
  75. session_no = (long)123456789012345
  76. };
  77. }
  78. void assert_private_matches(LoginPrivate expected, LoginPrivate actual, string label) {
  79. check(actual.user_id == expected.user_id, label + ": user_id int64 round-trip");
  80. check(actual.is_admin == expected.is_admin, label + ": is_admin bool round-trip");
  81. check(actual.attempts == expected.attempts, label + ": attempts int round-trip");
  82. check(actual.score == expected.score, label + ": score double round-trip");
  83. check(actual.username == expected.username, label + ": username string round-trip");
  84. check(actual.session_no == expected.session_no, label + ": session_no long round-trip");
  85. }
  86. void test_scalar_public_path() throws GLib.Error {
  87. var model = sample_private();
  88. var props = GObjectMapping.to_properties(model);
  89. var wire = new JsonElement.from_properties(props).stringify();
  90. check(wire.contains("\"user_id\":9007199254740993"), "public path: int64 serialises as an unquoted JSON number");
  91. check(wire.contains("\"is_admin\":true"), "public path: bool serialises as an unquoted JSON boolean");
  92. check(wire.contains("\"attempts\":3"), "public path: int serialises as an unquoted JSON number, not a string");
  93. check(wire.contains("\"score\":0.5"), "public path: double serialises as an unquoted JSON number");
  94. var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate),
  95. new JsonElement.from_string(wire).as<Properties>());
  96. assert_private_matches(model, back, "public path");
  97. }
  98. void test_scalar_encryption_path(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
  99. var model = sample_private();
  100. var blob = encryption.author_properties("statum-tests", GObjectMapping.to_properties(model));
  101. var props = encryption.read_properties("statum-tests", blob);
  102. var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate), props);
  103. assert_private_matches(model, back, "encryption path");
  104. }
  105. void test_arrays(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
  106. var first = new Model.ActionDto() { uri = "/_statum/action/aaaa", method = "POST" };
  107. var second = new Model.ActionDto() { uri = "/_statum/action/bbbb", method = "DELETE" };
  108. var refs = new Series<Model.ActionDto>();
  109. refs.add(first);
  110. refs.add(second);
  111. var model = new ArrayModel() { tags = new string[] { "alpha", "beta", "gamma" }, refs = refs };
  112. var props = GObjectMapping.to_properties(model);
  113. var wire = new JsonElement.from_properties(props).stringify();
  114. check(wire.contains("\"tags\":[\"alpha\",\"beta\",\"gamma\"]"), "arrays: string[] serialises as a JSON array");
  115. check(wire.contains("\"uri\":\"/_statum/action/bbbb\"")
  116. && wire.contains("\"method\":\"DELETE\""), "arrays: registered-Object collection serialises as an array of nested objects");
  117. var parsed = new JsonElement.from_string(wire).as<Properties>();
  118. var array_model = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel), parsed);
  119. check(array_model.tags.length == 3 && array_model.tags[0] == "alpha"
  120. && array_model.tags[1] == "beta" && array_model.tags[2] == "gamma", "arrays: string[] read-back from JSON array");
  121. var blob = encryption.author_properties("statum-tests", props);
  122. var decrypted = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel),
  123. encryption.read_properties("statum-tests", blob));
  124. check(decrypted.tags.length == 3 && decrypted.tags[2] == "gamma", "arrays: string[] round-trips through the encryption path");
  125. }
  126. void test_typed_action_private(Inversion.Scope scope, Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
  127. var eroding = (ErodingAction) scope.resolve_type(typeof(ErodingAction));
  128. check(eroding.exposed_private_type() == typeof(void), "S-8 repro: typeof(TPrivate) erodes to void for a reflectively-instantiated action");
  129. eroding.feed(new PropertyDictionary());
  130. check(eroding.exposed_request_private() == null, "S-8 repro: un-overridden request_private yields null without crashing");
  131. var fixed_action = (FixedAction) scope.resolve_type(typeof(FixedAction));
  132. check(fixed_action.exposed_private_type() == typeof(LoginPrivate), "S-8 fix: private_type override captures the GType from a foreign namespace");
  133. var model = sample_private();
  134. var blob = encryption.author_properties(fixed_action.get_type().name(), GObjectMapping.to_properties(model));
  135. fixed_action.feed(encryption.read_properties(fixed_action.get_type().name(), blob));
  136. var private_data = fixed_action.exposed_request_private();
  137. check(private_data != null, "S-8 fix: request_private materialises from a sealed blob");
  138. if (private_data != null) {
  139. assert_private_matches(model, (!)private_data, "S-8 fix");
  140. }
  141. }
  142. void main() {
  143. GObjectMapping.register_mapper<Model.ActionDto>(Model.ActionDto.get_mapper());
  144. var container = new Container();
  145. container.register_singleton<Statum.Cryptography.SigningProvider>();
  146. container.register_singleton<Statum.Cryptography.EncryptionProvider>();
  147. container.register_singleton<StateService>();
  148. container.register_singleton<HeldSlotResolver>();
  149. container.register_singleton<ChannelEndpoint>().as<ChannelService>();
  150. container.register_singleton<ActionRegistry>();
  151. container.register_singleton<TopicRegistry>();
  152. container.register_scoped<ErodingAction>();
  153. container.register_scoped<FixedAction>();
  154. var scope = container.create_transient_scope();
  155. var encryption = (Statum.Cryptography.EncryptionProvider) scope.resolve_type(typeof(Statum.Cryptography.EncryptionProvider));
  156. try {
  157. test_scalar_public_path();
  158. } catch (GLib.Error e) {
  159. failures++;
  160. print("FAIL scalar public path threw: %s\n", e.message);
  161. }
  162. try {
  163. test_scalar_encryption_path(encryption);
  164. } catch (GLib.Error e) {
  165. failures++;
  166. print("FAIL scalar encryption path threw: %s\n", e.message);
  167. }
  168. try {
  169. test_arrays(encryption);
  170. } catch (GLib.Error e) {
  171. failures++;
  172. print("FAIL arrays threw: %s\n", e.message);
  173. }
  174. try {
  175. test_typed_action_private(scope, encryption);
  176. } catch (GLib.Error e) {
  177. failures++;
  178. print("FAIL typed action private threw: %s\n", e.message);
  179. }
  180. print("---- %d passed, %d failed ----\n", passes, failures);
  181. if (failures > 0) {
  182. Process.exit(1);
  183. }
  184. }
  185. }