View raw

1 use v5.32.1; 2 use strict; 3 use warnings; 4 5 use Test2::V0; 6 use JSON::PP qw(decode_json); 7 8 use FindBin; 9 use lib "${FindBin::Bin}/../../../lib/"; 10 use lib "${FindBin::Bin}/../lib/"; 11 12 use FAPG::DAQ::MQTT qw( 13 publish_mqtt 14 publish_status_mqtt 15 mqtt_payload_for_reading 16 mqtt_payload_for_status 17 mqtt_topic_for_reading 18 mqtt_topic_for_status 19 mqtt_broker_endpoint 20 ); 21 22 { 23 24 package Local::FakeMQTT; 25 26 use v5.32.1; 27 use warnings; 28 29 sub new { 30 my ($class) = @_; 31 return bless { published => [] }, $class; 32 } 33 34 sub publish { 35 my ( $self, $topic, $payload ) = @_; 36 push $self->{published}->@*, [ $topic, $payload ]; 37 return 1; 38 } 39 40 sub published { 41 my ($self) = @_; 42 return $self->{published}; 43 } 44 } 45 46 subtest 'topic shape' => sub { 47 my $topic 48 = mqtt_topic_for_reading( 49 { probe => 'pH', value => 7.12, unit => 'pH' }, 50 node => 'fapg-daq-zero-01', ); 51 52 is $topic, 'fapg/daq/ph/fapg-daq-zero-01/reading'; 53 54 my $status_topic 55 = mqtt_topic_for_status( { probe => 'pH', status => 'ok' }, 56 node => 'fapg-daq-zero-01', ); 57 58 is $status_topic, 'fapg/daq/ph/fapg-daq-zero-01/status'; 59 }; 60 61 subtest 'payload shape' => sub { 62 my $payload = mqtt_payload_for_reading( 63 { probe => 'pH', 64 value => 7.12, 65 unit => 'pH', 66 raw => '7.12', 67 timestamp => '2026-07-06T10:00:00Z', 68 }, 69 node => 'fapg-daq-zero-01', 70 ); 71 72 my $decoded = decode_json($payload); 73 74 is $decoded->{schema}, 'fapg.daq.reading.v1'; 75 is $decoded->{timestamp}, '2026-07-06T10:00:00Z'; 76 is $decoded->{node}, 'fapg-daq-zero-01'; 77 is $decoded->{probe}, 'ph'; 78 is $decoded->{value}, 7.12; 79 is $decoded->{unit}, 'pH'; 80 is $decoded->{raw}, '7.12'; 81 is $decoded->{source}, 'fapg-daq-node'; 82 }; 83 84 subtest 'status payload shape' => sub { 85 my $payload = mqtt_payload_for_status( 86 { probe => 'pH', 87 status => 'probe_error', 88 error => 'timeout waiting for EZO response', 89 device => '/dev/serial/by-id/usb-FTDI_USB_UART', 90 timestamp => '2026-07-06T10:00:05Z', 91 }, 92 node => 'fapg-daq-zero-01', 93 ); 94 95 my $decoded = decode_json($payload); 96 97 is $decoded->{schema}, 'fapg.daq.status.v1'; 98 is $decoded->{timestamp}, '2026-07-06T10:00:05Z'; 99 is $decoded->{node}, 'fapg-daq-zero-01'; 100 is $decoded->{probe}, 'ph'; 101 is $decoded->{status}, 'probe_error'; 102 is $decoded->{error}, 'timeout waiting for EZO response'; 103 is $decoded->{device}, '/dev/serial/by-id/usb-FTDI_USB_UART'; 104 is $decoded->{source}, 'fapg-daq-node'; 105 }; 106 107 subtest 'hub status shape' => sub { 108 my $topic = mqtt_topic_for_status( 109 { probe => 'hub', 110 status => 'ok', 111 }, 112 node => 'fapg-daq-five-01', 113 ); 114 115 is $topic, 'fapg/daq/hub/fapg-daq-five-01/status'; 116 117 my $payload = mqtt_payload_for_status( 118 { probe => 'hub', 119 status => 'ok', 120 message => 'DAQ hub MQTT broker reachable', 121 timestamp => '2026-07-06T10:00:10Z', 122 }, 123 node => 'fapg-daq-five-01', 124 source => 'fapg-daq-hub', 125 ); 126 127 my $decoded = decode_json($payload); 128 129 is $decoded->{schema}, 'fapg.daq.status.v1'; 130 is $decoded->{timestamp}, '2026-07-06T10:00:10Z'; 131 is $decoded->{node}, 'fapg-daq-five-01'; 132 is $decoded->{probe}, 'hub'; 133 is $decoded->{status}, 'ok'; 134 is $decoded->{message}, 'DAQ hub MQTT broker reachable'; 135 is $decoded->{source}, 'fapg-daq-hub'; 136 }; 137 138 subtest 'publish uses injected client, so unit tests need no broker' => sub { 139 my $fake = Local::FakeMQTT->new; 140 141 my $result = publish_mqtt( 142 { probe => 'DO', 143 value => 8.34, 144 unit => 'mg/L', 145 timestamp => '2026-07-06T10:00:00Z', 146 }, 147 node => 'fapg-daq-zero-02', 148 client => $fake, 149 ); 150 151 is $result->{topic}, 'fapg/daq/do/fapg-daq-zero-02/reading'; 152 is scalar $fake->published->@*, 1; 153 is $fake->published->[0][0], 'fapg/daq/do/fapg-daq-zero-02/reading'; 154 155 my $decoded = decode_json( $fake->published->[0][1] ); 156 is $decoded->{probe}, 'do'; 157 is $decoded->{value}, 8.34; 158 159 my $status = publish_status_mqtt( 160 { probe => 'DO', 161 status => 'ok', 162 }, 163 node => 'fapg-daq-zero-02', 164 client => $fake, 165 ); 166 167 is $status->{topic}, 'fapg/daq/do/fapg-daq-zero-02/status'; 168 is scalar $fake->published->@*, 2; 169 is $fake->published->[1][0], 'fapg/daq/do/fapg-daq-zero-02/status'; 170 }; 171 172 subtest 'broker endpoint defaults to the Pi 5 broker' => sub { 173 local $ENV{FAPG_MQTT_HOST}; 174 local $ENV{MQTT_HOST}; 175 local $ENV{FAPG_MQTT_PORT}; 176 local $ENV{MQTT_PORT}; 177 178 is mqtt_broker_endpoint(), 'fapg-daq-five-01:1883'; 179 }; 180 181 done_testing; 182