From 1475f605aa8fb0efa2aed3f6a8e828939e6fbffd Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Thu, 28 May 2020 02:12:39 +0200
Subject: [PATCH] add: foreign key support in database add: more tests fix: bad
 tag handling when finding 0 in column

---
 helpers/bind_server.c                         |  52 -------
 helpers/get_port.c                            |  24 ----
 helpers/open_discovery_socket.c               |  57 --------
 include/helpers.h                             |  18 ---
 main.c                                        |   2 +
 models/junction_tag.c                         |  29 +++-
 models/relay.c                                |   2 +-
 sql/migration_0.sql                           |  15 +-
 .../controller_relays_basic.tavern.yaml       | 129 +++---------------
 tests/tavern_tests/tags.tavern.yaml           |  53 +++++++
 .../validate_controller.cpython-38.pyc        | Bin 0 -> 1907 bytes
 .../__pycache__/validate_relay.cpython-38.pyc | Bin 0 -> 2767 bytes
 tests/tavern_utils/validate_controller.py     |  41 ++++++
 tests/tavern_utils/validate_relay.py          |  68 ++++++++-
 14 files changed, 214 insertions(+), 276 deletions(-)
 delete mode 100644 helpers/bind_server.c
 delete mode 100644 helpers/get_port.c
 delete mode 100644 helpers/open_discovery_socket.c
 create mode 100644 tests/tavern_utils/__pycache__/validate_controller.cpython-38.pyc
 create mode 100644 tests/tavern_utils/__pycache__/validate_relay.cpython-38.pyc
 create mode 100644 tests/tavern_utils/validate_controller.py

diff --git a/helpers/bind_server.c b/helpers/bind_server.c
deleted file mode 100644
index b796d5b..0000000
--- a/helpers/bind_server.c
+++ /dev/null
@@ -1,52 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <netdb.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <errno.h>
-
-#include <logger.h>
-#include <helpers.h>
-
-int
-helper_bind_tcp_server(char* addr, uint16_t port, int max_client_backlog)
-{
-    char port_str[6];
-    sprintf(port_str, "%d", port);
-
-    struct addrinfo hints, *res;
-    int fd;
-    int status;
-
-    memset(&hints, 0, sizeof hints);
-    hints.ai_family = AF_UNSPEC;
-    hints.ai_socktype = SOCK_STREAM;
-    hints.ai_flags = AI_PASSIVE;
-
-    if ((status = getaddrinfo(addr, port_str, &hints, &res)) != 0)
-    {
-        LOG_ERROR("getaddrinfo: %s\n", gai_strerror(status));
-        return -1;
-    }
-
-    fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-
-    if ((status = bind(fd, res->ai_addr, res->ai_addrlen)) == -1)
-    {
-        LOG_ERROR("error binding socket: %s\n", strerror(errno));
-        freeaddrinfo(res);
-        return -1;
-    }
-
-    if ((status = listen(fd, max_client_backlog)) == -1)
-    {
-        LOG_ERROR("error setting up listener: %s\n", strerror(errno));
-        freeaddrinfo(res);
-        return -1;
-    }
-
-    freeaddrinfo(res);
-
-    return fd;
-}
diff --git a/helpers/get_port.c b/helpers/get_port.c
deleted file mode 100644
index 58c9ce8..0000000
--- a/helpers/get_port.c
+++ /dev/null
@@ -1,24 +0,0 @@
-#include <sys/socket.h>
-#include <arpa/inet.h>
-#include <stdint.h>
-#include <errno.h>
-#include <string.h>
-
-#include <helpers.h>
-#include <logger.h>
-
-uint16_t
-helper_get_port(int sock)
-{
-    struct sockaddr_in sin;
-    socklen_t len = sizeof(sin);
-    if (getsockname(sock, (struct sockaddr *)&sin, &len) == -1)
-    {
-        LOG_ERROR("could not get socket name for port: %s\n", strerror(errno));
-        return 0;
-    }
-    else
-    {
-        return ntohs(sin.sin_port);
-    }
-}
diff --git a/helpers/open_discovery_socket.c b/helpers/open_discovery_socket.c
deleted file mode 100644
index 855bc0a..0000000
--- a/helpers/open_discovery_socket.c
+++ /dev/null
@@ -1,57 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <errno.h>
-#include <netdb.h>
-#include <sys/socket.h>
-
-#include <logger.h>
-#include <helpers.h>
-
-int
-helper_open_discovery_socket(uint16_t discovery_port)
-{
-    struct addrinfo hints, *res;
-    int fd, status;
-
-    memset(&hints, 0, sizeof hints);
-    hints.ai_family = AF_INET; // use ipv4
-    hints.ai_socktype = SOCK_DGRAM; //set socket flag
-    hints.ai_flags = AI_PASSIVE; // get my IP
-
-    char discovery_port_str[6];
-    sprintf(discovery_port_str, "%u", discovery_port);
-
-    //get connection info for our computer
-    if ((status = getaddrinfo(NULL, discovery_port_str, &hints, &res)) != 0)
-    {
-        LOG_FATAL("getaddrinfo: %s\n", gai_strerror(status));
-        freeaddrinfo(res);
-        exit(EXIT_FAILURE);
-    }
-
-    //creating socket
-    fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-    int yes = 1;
-
-    // lose the pesky "Address already in use" error message
-    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
-    {
-        LOG_FATAL("setsockopt: %s\n", strerror(errno));
-        freeaddrinfo(res);
-        exit(EXIT_FAILURE);
-    }
-
-    if (bind(fd, res->ai_addr, res->ai_addrlen) == -1)
-    {
-        LOG_FATAL("bind: %s\n", strerror(errno));
-        freeaddrinfo(res);
-        exit(EXIT_FAILURE);
-    }
-
-    freeaddrinfo(res);
-
-    LOG_INFO("opened discovery socket on port %u\n", discovery_port);
-
-    return fd;
-}
diff --git a/include/helpers.h b/include/helpers.h
index f382679..45af161 100644
--- a/include/helpers.h
+++ b/include/helpers.h
@@ -8,24 +8,6 @@
 int
 helper_connect_tcp_server(char* host, uint16_t port);
 
-int
-helper_bind_tcp_server(char* addr, uint16_t port, int max_client_backlog);
-
-uint16_t
-helper_get_port(int sock);
-
-/**
- * @brief Open socket for discovery
- *
- * Will exit program when unable to open socket.
- *
- * @param discovery_port Port number to listen on for discovery broadcasts
- *
- * @return Open socket to accept discovery broadcasts on
- */
-int
-helper_open_discovery_socket(uint16_t discovery_port);
-
 void
 helper_parse_cli(int argc, const char **argv, config_t *config);
 
diff --git a/main.c b/main.c
index 24b7cab..c5b1a12 100644
--- a/main.c
+++ b/main.c
@@ -84,6 +84,8 @@ main(int argc, const char** argv)
         terminate(1);
     }
 
+    sqlite3_exec(global_database, "PRAGMA foreign_keys = ON", 0, 0, 0);
+
 
     /******************** INIT ROUTER ********************/
 
diff --git a/models/junction_tag.c b/models/junction_tag.c
index 9beddc1..c4c6a33 100644
--- a/models/junction_tag.c
+++ b/models/junction_tag.c
@@ -15,8 +15,24 @@ junction_tag_insert(int tag_id, int relay_id, int schedule_id)
     sqlite3_prepare_v2(global_database, "INSERT INTO junction_tag(tag_id, schedule_id, relay_id) values (?1, ?2, ?3);", -1, &stmt, NULL);
 
     sqlite3_bind_int(stmt, 1, tag_id);
-    sqlite3_bind_int(stmt, 2, schedule_id);
-    sqlite3_bind_int(stmt, 3, relay_id);
+
+    if(schedule_id)
+    {
+        sqlite3_bind_int(stmt, 2, schedule_id);
+    }
+    else
+    {
+        sqlite3_bind_null(stmt, 2);
+    }
+
+    if(relay_id)
+    {
+        sqlite3_bind_int(stmt, 3, relay_id);
+    }
+    else
+    {
+        sqlite3_bind_null(stmt, 3);
+    }
 
     rc = sqlite3_step(stmt);
     if (rc != SQLITE_DONE)
@@ -46,10 +62,13 @@ get_ids(sqlite3_stmt *stmt)
         if (s == SQLITE_ROW)
         {
             new_id = sqlite3_column_int(stmt, 0);
-            row++;
+            if(new_id != 0) // found row for other target (relay <> schedule)
+            {
+                row++;
 
-            ids = (int*)realloc(ids, sizeof(int) * (row + 1));
-            ids[row - 1] = new_id;
+                ids = (int*)realloc(ids, sizeof(int) * (row + 1));
+                ids[row - 1] = new_id;
+            }
         }
         else
         {
diff --git a/models/relay.c b/models/relay.c
index 3e364dd..75632e6 100644
--- a/models/relay.c
+++ b/models/relay.c
@@ -236,7 +236,7 @@ relay_to_json(relay_t *relay)
     controller_t *controller = controller_get_by_id(relay->controller_id);
     if(!controller)
     {
-        LOG_DEBUG("failed to get controller\n");
+        LOG_WARN("failed to get controller\n");
         cJSON_Delete(json);
         return NULL;
     }
diff --git a/sql/migration_0.sql b/sql/migration_0.sql
index cd17b3d..36246d1 100644
--- a/sql/migration_0.sql
+++ b/sql/migration_0.sql
@@ -31,6 +31,7 @@ create table relays
     controller_id   INTEGER
                     NOT NULL
                     REFERENCES controllers (id)
+                    ON DELETE CASCADE
 );
 
 create table schedules
@@ -59,11 +60,14 @@ create table junction_tag
 (
     tag_id          INTEGER
                     NOT NULL
-                    REFERENCES tags (id),
+                    REFERENCES tags (id)
+                    ON DELETE CASCADE,
     relay_id        INTEGER
-                    REFERENCES relays (id),
+                    REFERENCES relays (id)
+                    ON DELETE CASCADE,
     schedule_id     INTEGER
                     REFERENCES schedules (id)
+                    ON DELETE CASCADE
 );
 
 create table junction_relay_schedule
@@ -71,11 +75,12 @@ create table junction_relay_schedule
     weekday         SMALLINT
                     NOT NULL,
     relay_id        INTEGER
-                    NOT NULL
-                    REFERENCES relays (id),
+                    REFERENCES relays (id)
+                    ON DELETE CASCADE,
     schedule_id     INTEGER
-                    NOT NULL
+                    DEFAULT 1
                     REFERENCES schedules (id)
+                    ON DELETE SET DEFAULT
 );
 
 INSERT INTO schedules (uid, name, periods) VALUES (x'6f666600000000000000000000000000', 'off', x'00');
diff --git a/tests/tavern_tests/controller_relays_basic.tavern.yaml b/tests/tavern_tests/controller_relays_basic.tavern.yaml
index ee59c11..df2bed1 100644
--- a/tests/tavern_tests/controller_relays_basic.tavern.yaml
+++ b/tests/tavern_tests/controller_relays_basic.tavern.yaml
@@ -7,18 +7,12 @@ stages:
     url: "http://localhost:5000/api/v1/controllers/discover/"
   response:
     status_code: 200
-    json:
-    - id: !anystr
-      name: !anystr
-      relay_count: !anyint
-      relays: !anystr
-      active: !anybool
-      port: !anyint
-      ip: !anystr
-      relays: !anylist
+    verify_response_with:
+      function: validate_controller:multiple
     save:
       json:
         returned_id: "[0].id"
+        returned_relay_count: "[0].relay_count"
 
 - name: "[controller_relays_basic] get controller relays, check length"
   request:
@@ -26,71 +20,11 @@ stages:
     url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays"
   response:
     status_code: 200
-    json:
-    - name: !anystr
-      number: 0
-      controller_id: "{returned_id}"
-      active_schedule:
-        id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 1
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 2
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 3
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 4
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 5
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 6
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 7
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 8
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
-    - name: !anystr
-      number: 9
-      controller_id: "{returned_id}"
-      active_schedule: !anydict
-      schedules: !anylist
-      tags: !anylist
+    verify_response_with:
+      function: validate_relay:multiple
+      function: validate_relay:relay_count
+      extra_kwargs:
+        relay_count: !int "{returned_relay_count:d}"
 
 - name: "[controller_relays_basic] get controller relays, check length"
   request:
@@ -98,42 +32,11 @@ stages:
     url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays/5"
   response:
     status_code: 200
-    json:
-      name: !anystr
-      number: 5
-      controller_id: "{returned_id}"
-      active_schedule:
-        id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      schedules: 
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      - id: !anystr
-        name: !anystr
-        periods: !anylist
-        tags: !anylist
-      tags: !anylist
+    verify_response_with:
+      function: validate_relay:single
+      function: validate_relay:check_controller_id
+      extra_kwargs:
+        name: "{returned_id}"
+      function: validate_relay:check_number
+      extra_kwargs:
+        number: 5
diff --git a/tests/tavern_tests/tags.tavern.yaml b/tests/tavern_tests/tags.tavern.yaml
index f3576aa..57c18a1 100644
--- a/tests/tavern_tests/tags.tavern.yaml
+++ b/tests/tavern_tests/tags.tavern.yaml
@@ -44,3 +44,56 @@ stages:
         id: "{returned_id}"
         name: "{returned_name}"
         periods: "{returned_periods}"
+
+- name: "[tags] discover controllers"
+  request:
+    method: POST
+    url: "http://localhost:5000/api/v1/controllers/discover/"
+  response:
+    status_code: 200
+    verify_response_with:
+      function: validate_controller:multiple
+    save:
+      json:
+        returned_id: "[0].id"
+
+- name: "[tags] set relay tag"
+  request:
+    method: PUT
+    url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays/3"
+    json:
+      tags:
+      - "test_tag_1"
+  response:
+    status_code: 200
+    verify_response_with:
+      function: validate_relay:single
+      function: validate_relay:check_controller_id
+      extra_kwargs:
+        name: "{returned_id}"
+      function: validate_relay:check_number
+      extra_kwargs:
+        number: 3
+      function: validate_relay:check_tag
+      extra_kwargs:
+        tag: "{tavern.request_vars.json.tags[0]}"
+    save:
+      json:
+        returned_name: "name"
+        returned_number: "number"
+        returned_tag: "tags[0]"
+
+- name: "[tags] get relay, check name and number"
+  request:
+    method: GET
+    url: "http://localhost:5000/api/v1/relays/tag/{returned_tag}"
+  response:
+    status_code: 200
+    verify_response_with:
+      function: validate_relay:multiple
+      function: validate_relay:find
+      extra_kwargs:
+        name: "{returned_name}"
+        number: !int "{returned_number:d}"
+        controller_id: "{returned_id}"
+        tag: "{returned_tag}"
diff --git a/tests/tavern_utils/__pycache__/validate_controller.cpython-38.pyc b/tests/tavern_utils/__pycache__/validate_controller.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..419f175c2dd8841e067cda593d862bcc66da9797
GIT binary patch
literal 1907
zcmbtU&yO256drqKlG$vM(gMp;pv;k6NL2}m145ue#R264NL3Jo>DZficQTW;ovk)X
zCDiSyf5qn5|AZ5Nz+5?T;|eFf=gE)BmKz@VdHlWi?9cXl?}tvO<)Quk*Y7|7*zmkR
zXi{%JCSRbNM-ajjEc1rd$c9XC;lJ>PTm&M-=!=GEVhlti4lstIB@Qt*L|3#$=LH)!
z1^dqH-TD)2d4rVEux=b<i2e}W{03pYrFYE)TQSS8xH-1I;8*N9TjTy3zYjd9;UVw<
z_TK<+)bJ+o5cn^^BZoKK`oOQ4`-A5hH@`xEK>CgLEAOXIpV6+&HsOB_earbr*9S|m
zW6*%-;i~cgf>mBx|2aE<DLkCEJt(;nix1Kwx4OtOsbeK#W#YWBaS|J=RX#3*JekVH
zhczPtZki1bwak*sQCiG%yZESPUBkCdd8~3P$5Ia)PHpzqub{1QoGD`$pWIwBo+K9%
zG&7lx?IfP(@_e3Tu`S}-j<36!m=;P@4p(OwKb@#_vfE9l)DAnl-9}3ET4hTal^dJn
zsVx1mw55;hDZ5`9BehjU{<YSHE`1!U42W;(gI5{QVrBOt$&^T}9Jvk4?&v~lb#ghv
zlH*MFc-h)*Hw>N8D^K4-to`$Ce^N|k-xg0)V)`>(oXXUizMPJe`O~DI78*)v09jmz
z&PQ{rGSlDgxLf$+*`@Aa$$;Yc4ur>!>Ep*NW?goNwV2cI1wSQ%kNyzdya7=K`O3RO
zc)1{AgC2K&WpHYWyvNE&OEW8STnwF{669Jos`Jvnp{(+leiL$UL&CQk-p6FKq06-+
z#5l5|%7K-K3}_<*f+aBKo!@hW_+W9gVX%8udXrwL>G1kCi31W%5_Bmz16_&klDI)T
zn$9z;W|<_i2BiqZk64@OxA2#`gAj(cNB$lFq^h@C7mFpO-(AbAV&SOz15GZAySrDS
z3`{37J&R8gl}Yi+ujo<*a&$LL$?-L&&JzK94CUsEkMRG0S<;Iv-mkHAC7{kzzYUWO
zH!{D;tyOU&$M>M#uzLspDcNrtz0kJY=9pA>h^PMknZIPhJM;AelstCLsRYx^`kR>x
zgJ{W})#D|1ICoZ$*H)NSR#p58-<;Z@D%9>x!s<;laeB0WW>m!!lsA;f+|?w+6NS<+
zGB}%<qka<cGMu4q+Ul`_Dy-aNxm)?K01Hl3E+|~qY1u}2bs6VQvIf)dLf7MiK`*L;
vp^do<TbDmB4mCZqrrUJZwcB^m?D8-AY+4Y_&nc@7F^O2jqjuy+A-exB5u(Bt

literal 0
HcmV?d00001

diff --git a/tests/tavern_utils/__pycache__/validate_relay.cpython-38.pyc b/tests/tavern_utils/__pycache__/validate_relay.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f544ec03a7d5782a40dcef2d4d0d84464e10fcb2
GIT binary patch
literal 2767
zcmbtW%WmXE6m{8dx6|o#o|9xUFY}O4Ba}oeV1p0{7_#aOuxW|uI4-B#6UR<hxigB@
z&4SE=)qF#O*o=h4Kkxy3fVW6UvzsMi#kp1Oeo3<$TlG5kR@JFn_g?#lm6e)-=Z}B?
zdUAr^-!zzgYz)3Ykxx)@gR|IZ>z1_{H@WrPXq(*T6|@$2conV9UA~02!fSjPt;6el
z1#Oi#_$peLukm%XOMHuO@XhC}UE}OKqj~2Q_Apujqh|bAXe%gRp~yNUnK3kO7-zRE
zGp|j#n_1kvW<RqrZUMJ}9|E^$@X8oJ0`AP<Rp1rq9|Lz4?%Y(>4v^e3R_?b1Jr{bv
zK(D6smXsFoYsQz)O!+G$b&{0$*T#?DNbyoT{0aHWWCyi&|MxNL#(4FAptCwz11Q$m
z9+}WG)u~=XzDDx?Yi__|>#f2`2InWC$RCO@_OCoW@*?RaY3BKf7bRKP4Mkxm{&|@1
zjkVC9=y);{QPM3|f;7oQ8pomNL_B{_tImOobK0wZkVThaM+Uu+58|+#wE$1&@hHgJ
zOQpKZALv~sz*IAiWcI3xYtNhWI$qDe3_ahA!=#(_yg?H72Y&2jsrPCA$-Juch`t{s
zP%pRep7o-jH{FLA6?W!#WxlItQ-3Na9{K)^WV$327gy7^Y}SfeB%?%TeiDR*)eW=4
z!c7#7FQp7c7NyBILZqUwpkLUE!uq8jN8Hb*#w;40%TPq8R~;<Xjl;qwE`^1IH?5+g
zC>3sbuEGMHmxkDa$^QHHpqHM92U&U&`SRdGq))>jlLz5>*B?Cd4}w%c7)n4Hj46`N
zAd6yoFrlE<-oLmK4Xj2vK<=S3*e<>1E?Yy{V-2>!>gY9?(whrh3StUg6}W5YXji{A
zt`Stqd0DGzs(FR|RHjLj6|M;7MVi1ubO>GDL9cMM0k+YXCMtu8UDR_M5<c1R5QB0<
zl_$0lIfzspowo+!s*c#ShrpQ8f2atVt$e#=Fn|BeiXcxEL_n-kMOjmk#~F60zfRQx
zZTEZ-XVFC*-i1&)s0_Bv>P*l$PzNCl&m8&t07~-LY{-yYb~97cFNxlp5bgPJ)<oUh
zI3${uAcJeRM0CutSXS5C3DN-qeV(hJ`+bN~+Ly*}rbwE|bH?pB7b}u`AgOY(B5Ab*
zohrYpLuZ_rkSBsO@6?atkiWDvMHgzyprtKo-U7d(u4Z=hGVeg4BudtLTcYGQ`OcUq
z<;Gc>;vS?WHPT$9RvS~p#P=aCi9JC7ZHZCn<(?)spNZedkftm$+|&%`R`_6sDqZIs
zRXXM*m_Ei}_OcN?8zXaQ(xr=rj^=)Yn{poxU{R1M2e4a$T>f#sLwS-uN2hQ+4rkQ|
z`bZnr(($ti)uLW{bq7-x53!^V0m+BZmE<)B5|E2mc<2Z8pHMy|o%}cahQTzTnEHcI
zIpC*JWoUBa%o2w<PUQwccFSng-x}f&=p7B2!cB!A0k;%>JhT*!{|#sz1Gg3ag4-iU
zN$|%3L#>Uf!|Kq6p)R>Ia{8A;N51H9X`^vl-XJekCnj@s%n0RWbdish3BRBC=ByFv
zW6-pV$_2jO*<vKQ+Nx?H6;8@6$jB;Fwb#{MbfTnFnp`|0;e1vo?9(XWluYHjs0`$;
zIx?7Pmc?Z@vcJn5v?gjnYE8R^r@5>-tpHJ(Ql)H_`c$$J_enrOBFO#J6{_ho926V%
c1#mw*Pw5BabNa4G!g24WyWv`H1x3Hk|HPq!P5=M^

literal 0
HcmV?d00001

diff --git a/tests/tavern_utils/validate_controller.py b/tests/tavern_utils/validate_controller.py
new file mode 100644
index 0000000..7ad0e9c
--- /dev/null
+++ b/tests/tavern_utils/validate_controller.py
@@ -0,0 +1,41 @@
+import json
+import validate_relay
+
+def _verify_single(controller):
+    assert isinstance(controller.get("id"), str), "controller id is not a string"
+    assert isinstance(controller.get("name"), str), "controller name is not a string"
+    assert isinstance(controller.get("relay_count"), int), "controller relay_count is not an integer"
+
+    assert isinstance(controller.get("relays"), list), "controller relays is not a list"
+    assert len(controller.get("relays")) == controller.get("relay_count"), "controller relay have a length unequal to relay_count"
+    for relay in controller.get("relays"):
+        assert isinstance(relay, dict), "controller relays contain a relay which is not a dict"
+        validate_relay._verify_single(relay)
+        assert relay.get("controller_id") == controller.get("id")
+
+def single(response):
+    _verify_single(response.json())
+
+def multiple(response):
+    assert isinstance(response.json(), list), "response is not a list"
+    for controller in response.json():
+        _verify_single(controller)
+
+def check_id(response, id):
+    assert response.json().get("id") == id, "controller id check failed"
+
+def check_name(response, name):
+    assert response.json().get("name") == name, "controller name check failed"
+
+def find(response, id=None, name=None):
+    for controller in response.json():
+        if id != None and id != schedule.get("id"):
+            print(schedule.get("id"))
+            continue
+
+        if name != None and name != schedule.get("name"):
+            print(schedule.get("name"))
+            continue
+        return
+    assert False, "controller not found in list"
+
diff --git a/tests/tavern_utils/validate_relay.py b/tests/tavern_utils/validate_relay.py
index 0ae475a..39f3811 100644
--- a/tests/tavern_utils/validate_relay.py
+++ b/tests/tavern_utils/validate_relay.py
@@ -1,2 +1,68 @@
+import json
+import validate_schedule
+
+def _verify_single(relay):
+    assert isinstance(relay.get("number"), int), "relay number is not an integer"
+    assert isinstance(relay.get("name"), str), "relay name is not a string"
+    assert isinstance(relay.get("controller_id"), str), "relay controller_id is not a string"
+
+    assert isinstance(relay.get("active_schedule"), dict), "relay active_schedule is not a dict"
+    validate_schedule._verify_single(relay.get("active_schedule"))
+
+    assert isinstance(relay.get("schedules"), list), "relay schedules is not a list"
+    assert len(relay.get("schedules")) == 7, "relay schedule have a length unequal to 7"
+    for schedule in relay.get("schedules"):
+        assert isinstance(relay, dict), "relay schedules contain a schedule which is not a dict"
+        validate_schedule._verify_single(schedule)
+
+    assert isinstance(relay.get("tags"), list), "relay tags is not a list"
+    for tag in relay.get("tags"):
+        assert isinstance(tag, str), "relay tags contain a tag which is not a string"
+
 def single(response):
-    assert response.json().get("number") >= 0
+    _verify_single(response.json())
+
+def multiple(response):
+    assert isinstance(response.json(), list), "response is not a list"
+    for relay in response.json():
+        _verify_single(relay)
+
+def relay_count(response, relay_count):
+    assert len(response.json()) == relay_count, "response has invalid length"
+
+def check_number(response, number):
+    assert response.json().get("number") == number, "relay number check failed"
+
+def check_name(response, name):
+    assert response.json().get("name") == name, "relay name check failed"
+
+def check_controller_id(response, controller_id):
+    assert response.json().get("controller_id") == controller_id, "relay controller_id check failed"
+
+def check_tag(response, tag):
+    for response_tag in response.json().get("tags"):
+        if response_tag == tag:
+            return
+    assert False, "tag not found in relay,"
+
+def find(response, name=None, number=None, controller_id=None, tag=None):
+    print(response.json())
+    for relay in response.json():
+        if number != None and number != relay.get("number"):
+            continue
+
+        if name != None and name != relay.get("name"):
+            continue
+
+        if controller_id != None and controller_id != relay.get("controller_id"):
+            continue
+
+        if tag != None:
+            found_in_response = False
+            for response_tag in relay.get("tags"):
+                if response_tag == tag:
+                    found_in_response = True
+            if not found_in_response:
+                continue
+        return
+    assert False, "relay not found in list"