openc3-cosmos-cfdp 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/microservices/CFDP/Gemfile +3 -11
  4. data/microservices/CFDP/Gemfile.dev +35 -0
  5. data/microservices/CFDP/Gemfile.prod +29 -0
  6. data/microservices/CFDP/app/controllers/cfdp_controller.rb +8 -0
  7. data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +3 -1
  8. data/microservices/CFDP/app/models/cfdp_mib.rb +2 -1
  9. data/microservices/CFDP/app/models/cfdp_pdu.rb +43 -19
  10. data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +7 -2
  11. data/microservices/CFDP/app/models/cfdp_source_transaction.rb +6 -1
  12. data/microservices/CFDP/app/models/cfdp_transaction.rb +1 -1
  13. data/microservices/CFDP/app/models/cfdp_user.rb +14 -4
  14. data/microservices/CFDP/config/application.rb +0 -11
  15. data/microservices/CFDP/config/environments/test.rb +1 -1
  16. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +6 -2
  17. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +17 -5
  18. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +28 -11
  19. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +5 -2
  20. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +16 -9
  21. data/microservices/CFDP/spec/controllers/cfdp_controller_spec.rb +373 -0
  22. data/microservices/CFDP/spec/models/cfdp_crc_checksum_spec.rb +134 -0
  23. data/microservices/CFDP/spec/models/cfdp_mib_spec.rb +389 -0
  24. data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +102 -1
  25. data/microservices/CFDP/spec/models/cfdp_pdu_user_ops_spec.rb +562 -0
  26. data/microservices/CFDP/spec/models/cfdp_receive_transaction_spec.rb +598 -0
  27. data/microservices/CFDP/spec/models/cfdp_transaction_spec.rb +331 -0
  28. data/microservices/CFDP/spec/models/cfdp_user_spec.rb +575 -0
  29. metadata +15 -6
@@ -0,0 +1,598 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # Licensed for Evaluation and Educational Use
7
+ #
8
+ # This file may only be used commercially under the terms of a commercial license
9
+ # purchased from OpenC3, Inc.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+ #
15
+ # The development of this software was funded in-whole or in-part by Sandia National Laboratories.
16
+ # See https://github.com/OpenC3/openc3-cosmos-cfdp/pull/12 for details
17
+
18
+ require 'rails_helper'
19
+ require 'tempfile'
20
+
21
+ RSpec.describe CfdpReceiveTransaction do
22
+ before(:each) do
23
+ # Mock the CfdpTopic static methods directly
24
+ allow(CfdpTopic).to receive(:write_indication)
25
+ ENV['OPENC3_MICROSERVICE_NAME'] = 'DEFAULT__API__CFDP'
26
+ ENV['OPENC3_SCOPE'] = 'DEFAULT'
27
+
28
+ allow(OpenC3::Logger).to receive(:info)
29
+
30
+ @transactions = {}
31
+ allow(CfdpMib).to receive(:transactions).and_return(@transactions)
32
+ allow(CfdpMib).to receive(:put_destination_file).and_return(true)
33
+ allow(CfdpMib).to receive(:filestore_request).and_return(["SUCCESSFUL", "Success"])
34
+
35
+ @source_entity = {
36
+ 'id' => 1,
37
+ 'name' => 'SOURCE',
38
+ 'protocol_version' => 1,
39
+ 'ack_timer_interval' => 5,
40
+ 'ack_timer_expiration_limit' => 3,
41
+ 'enable_acks' => true,
42
+ 'enable_finished' => true,
43
+ 'check_interval' => 5,
44
+ 'check_limit' => 3,
45
+ 'immediate_nak_mode' => true,
46
+ 'enable_eof_nak' => true,
47
+ 'nak_timer_interval' => 5,
48
+ 'nak_timer_expiration_limit' => 3,
49
+ 'keep_alive_interval' => 5,
50
+ 'enable_keep_alive' => true,
51
+ 'transaction_inactivity_limit' => 3,
52
+ 'incomplete_file_disposition' => 'DISCARD',
53
+ 'cmd_info' => ['TARGET', 'PACKET', 'ITEM'],
54
+ 'fault_handler' => {
55
+ 'NO_ERROR' => 'IGNORE_ERROR',
56
+ 'FILESTORE_REJECTION' => 'IGNORE_ERROR',
57
+ 'FILE_CHECKSUM_FAILURE' => 'IGNORE_ERROR',
58
+ 'CHECK_LIMIT_REACHED' => 'IGNORE_ERROR',
59
+ 'NAK_LIMIT_REACHED' => 'IGNORE_ERROR',
60
+ 'INACTIVITY_DETECTED' => 'IGNORE_ERROR',
61
+ 'ACK_LIMIT_REACHED' => 'IGNORE_ERROR',
62
+ 'CANCEL_REQUEST_RECEIVED' => 'IGNORE_ERROR'
63
+ },
64
+ 'maximum_file_segment_length' => 64
65
+ }
66
+
67
+ @destination_entity = {
68
+ 'id' => 2,
69
+ 'name' => 'DESTINATION',
70
+ 'protocol_version' => 1,
71
+ 'ack_timer_interval' => 5,
72
+ 'ack_timer_expiration_limit' => 3,
73
+ 'enable_acks' => true,
74
+ 'enable_finished' => true,
75
+ 'check_interval' => 5,
76
+ 'check_limit' => 3,
77
+ 'immediate_nak_mode' => true,
78
+ 'enable_eof_nak' => true,
79
+ 'file_segment_recv_indication' => true,
80
+ 'eof_recv_indication' => true,
81
+ 'transaction_finished_indication' => true,
82
+ 'nak_timer_interval' => 5,
83
+ 'nak_timer_expiration_limit' => 3,
84
+ 'maximum_file_segment_length' => 64,
85
+ 'fault_handler' => {
86
+ 'NO_ERROR' => 'IGNORE_ERROR',
87
+ 'FILESTORE_REJECTION' => 'IGNORE_ERROR',
88
+ 'FILE_CHECKSUM_FAILURE' => 'IGNORE_ERROR',
89
+ 'CHECK_LIMIT_REACHED' => 'IGNORE_ERROR',
90
+ 'NAK_LIMIT_REACHED' => 'IGNORE_ERROR',
91
+ 'INACTIVITY_DETECTED' => 'IGNORE_ERROR',
92
+ 'ACK_LIMIT_REACHED' => 'IGNORE_ERROR',
93
+ 'CANCEL_REQUEST_RECEIVED' => 'IGNORE_ERROR'
94
+ }
95
+ }
96
+
97
+ allow(CfdpMib).to receive(:entity).with(nil).and_return(@source_entity)
98
+ allow(CfdpMib).to receive(:entity).with(1).and_return(@source_entity)
99
+ allow(CfdpMib).to receive(:source_entity).and_return(@destination_entity)
100
+
101
+ @metadata_pdu_hash = {
102
+ "DIRECTIVE_CODE" => "METADATA",
103
+ "SOURCE_ENTITY_ID" => 1,
104
+ "SEQUENCE_NUMBER" => 123,
105
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
106
+ "FILE_SIZE" => 100,
107
+ "SOURCE_FILE_NAME" => "source.txt",
108
+ "DESTINATION_FILE_NAME" => "destination.txt",
109
+ "CLOSURE_REQUESTED" => "CLOSURE_REQUESTED",
110
+ "CHECKSUM_TYPE" => 0
111
+ }
112
+
113
+ @file_data_pdu_hash = {
114
+ "DIRECTIVE_CODE" => "FILE_DATA",
115
+ "SOURCE_ENTITY_ID" => 1,
116
+ "SEQUENCE_NUMBER" => 123,
117
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
118
+ "OFFSET" => 0,
119
+ "FILE_DATA" => "a" * 50
120
+ }
121
+
122
+ @file_data_pdu_hash2 = {
123
+ "DIRECTIVE_CODE" => "FILE_DATA",
124
+ "SOURCE_ENTITY_ID" => 1,
125
+ "SEQUENCE_NUMBER" => 123,
126
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
127
+ "OFFSET" => 50,
128
+ "FILE_DATA" => "b" * 50
129
+ }
130
+
131
+ @eof_pdu_hash = {
132
+ "DIRECTIVE_CODE" => "EOF",
133
+ "SOURCE_ENTITY_ID" => 1,
134
+ "SEQUENCE_NUMBER" => 123,
135
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
136
+ "CONDITION_CODE" => "NO_ERROR",
137
+ "FILE_SIZE" => 100,
138
+ "FILE_CHECKSUM" => 0
139
+ }
140
+
141
+ @ack_pdu_hash = {
142
+ "DIRECTIVE_CODE" => "ACK",
143
+ "SOURCE_ENTITY_ID" => 1,
144
+ "SEQUENCE_NUMBER" => 123,
145
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
146
+ "DIRECTIVE_CODE_OF_ACK" => "FINISHED",
147
+ "CONDITION_CODE" => "NO_ERROR",
148
+ "TRANSACTION_STATUS" => "TERMINATED"
149
+ }
150
+
151
+ @prompt_pdu_hash = {
152
+ "DIRECTIVE_CODE" => "PROMPT",
153
+ "SOURCE_ENTITY_ID" => 1,
154
+ "SEQUENCE_NUMBER" => 123,
155
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
156
+ "RESPONSE_REQUIRED" => "NAK"
157
+ }
158
+
159
+ @prompt_pdu_hash2 = {
160
+ "DIRECTIVE_CODE" => "PROMPT",
161
+ "SOURCE_ENTITY_ID" => 1,
162
+ "SEQUENCE_NUMBER" => 123,
163
+ "TRANSMISSION_MODE" => "ACKNOWLEDGED",
164
+ "RESPONSE_REQUIRED" => "KEEP_ALIVE"
165
+ }
166
+
167
+ @mock_tempfile = double("Tempfile")
168
+ allow(@mock_tempfile).to receive(:seek)
169
+ allow(@mock_tempfile).to receive(:write)
170
+ allow(@mock_tempfile).to receive(:close)
171
+ allow(@mock_tempfile).to receive(:unlink)
172
+ allow(Tempfile).to receive(:new).and_return(@mock_tempfile)
173
+
174
+ # Mock checksum classes
175
+ @mock_null_checksum = double("CfdpNullChecksum")
176
+ allow(@mock_null_checksum).to receive(:add)
177
+ allow(@mock_null_checksum).to receive(:check).and_return(true)
178
+ allow(CfdpNullChecksum).to receive(:new).and_return(@mock_null_checksum)
179
+
180
+ # Mock cmd method from OpenC3::Api that's included in CfdpTransaction
181
+ allow_any_instance_of(CfdpReceiveTransaction).to receive(:cmd)
182
+
183
+ # Mock get_checksum method
184
+ allow_any_instance_of(CfdpReceiveTransaction).to receive(:get_checksum).and_return(@mock_null_checksum)
185
+
186
+ # Mock CfdpPdu build methods
187
+ allow(CfdpPdu).to receive(:build_ack_pdu).and_return("ACK_PDU")
188
+ allow(CfdpPdu).to receive(:build_finished_pdu).and_return("FINISHED_PDU")
189
+ allow(CfdpPdu).to receive(:build_nak_pdu).and_return("NAK_PDU")
190
+ allow(CfdpPdu).to receive(:build_keep_alive_pdu).and_return("KEEP_ALIVE_PDU")
191
+ end
192
+
193
+ describe "initialize" do
194
+ it "initializes with metadata PDU" do
195
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
196
+ expect(receive_transaction.id).to eq("1__123")
197
+ expect(receive_transaction.instance_variable_get(:@transaction_seq_num)).to eq(123)
198
+ expect(receive_transaction.instance_variable_get(:@transmission_mode)).to eq("ACKNOWLEDGED")
199
+ expect(receive_transaction.instance_variable_get(:@metadata_pdu_hash)).to eq(@metadata_pdu_hash)
200
+ end
201
+ end
202
+
203
+ describe "handle_pdu" do
204
+ it "handles metadata PDU" do
205
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
206
+ expect(receive_transaction.instance_variable_get(:@metadata_pdu_hash)).to eq(@metadata_pdu_hash)
207
+ expect(receive_transaction.instance_variable_get(:@source_entity_id)).to eq(1)
208
+ expect(receive_transaction.instance_variable_get(:@file_size)).to eq(100)
209
+ expect(receive_transaction.instance_variable_get(:@source_file_name)).to eq("source.txt")
210
+ expect(receive_transaction.instance_variable_get(:@destination_file_name)).to eq("destination.txt")
211
+ end
212
+
213
+ it "handles file data PDU" do
214
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
215
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
216
+ expect(receive_transaction.instance_variable_get(:@progress)).to eq(50)
217
+ expect(receive_transaction.instance_variable_get(:@segments)[0]).to eq(50)
218
+ end
219
+
220
+ it "handles EOF PDU" do
221
+ allow(CfdpMib).to receive(:put_destination_file).and_return(true)
222
+
223
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
224
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
225
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
226
+ receive_transaction.handle_pdu(@eof_pdu_hash)
227
+
228
+ expect(receive_transaction.instance_variable_get(:@eof_pdu_hash)).to eq(@eof_pdu_hash)
229
+ expect(receive_transaction.instance_variable_get(:@state)).to eq("FINISHED")
230
+ expect(receive_transaction.instance_variable_get(:@transaction_status)).to eq("TERMINATED")
231
+ end
232
+
233
+ it "handles ACK PDU" do
234
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
235
+ receive_transaction.handle_pdu(@ack_pdu_hash)
236
+ expect(receive_transaction.instance_variable_get(:@finished_ack_pdu_hash)).to eq(@ack_pdu_hash)
237
+ expect(receive_transaction.instance_variable_get(:@finished_ack_timeout)).to be_nil
238
+ end
239
+
240
+ it "handles PROMPT PDU with NAK response" do
241
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
242
+ expect(receive_transaction).to receive(:send_naks)
243
+ receive_transaction.handle_pdu(@prompt_pdu_hash)
244
+ expect(receive_transaction.instance_variable_get(:@prompt_pdu_hash)).to eq(@prompt_pdu_hash)
245
+ end
246
+
247
+ it "handles PROMPT PDU with KEEP_ALIVE response" do
248
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
249
+ expect(receive_transaction).to receive(:send_keep_alive)
250
+ receive_transaction.handle_pdu(@prompt_pdu_hash2)
251
+ expect(receive_transaction.instance_variable_get(:@prompt_pdu_hash)).to eq(@prompt_pdu_hash2)
252
+ end
253
+
254
+ it "ignores unexpected PDU types" do
255
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
256
+ unexpected_pdu = {
257
+ "DIRECTIVE_CODE" => "NAK",
258
+ "SOURCE_ENTITY_ID" => 1,
259
+ "SEQUENCE_NUMBER" => 123
260
+ }
261
+ # No expectations, just confirming it doesn't error
262
+ receive_transaction.handle_pdu(unexpected_pdu)
263
+ end
264
+ end
265
+
266
+ describe "check_complete" do
267
+ it "returns false if metadata or EOF not received" do
268
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
269
+ # Clear metadata_pdu_hash
270
+ receive_transaction.instance_variable_set(:@metadata_pdu_hash, nil)
271
+ expect(receive_transaction.check_complete).to be false
272
+ end
273
+
274
+ it "handles canceled transaction" do
275
+ canceled_eof = @eof_pdu_hash.dup
276
+ canceled_eof["CONDITION_CODE"] = "CANCELED_BY_SOURCE"
277
+
278
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
279
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, canceled_eof)
280
+
281
+ expect(receive_transaction.check_complete).to be true
282
+ expect(receive_transaction.instance_variable_get(:@state)).to eq("CANCELED")
283
+ expect(receive_transaction.instance_variable_get(:@transaction_status)).to eq("TERMINATED")
284
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("CANCELED_BY_SOURCE")
285
+ end
286
+
287
+ it "completes file transfer with successful checksum" do
288
+ allow(CfdpMib).to receive(:put_destination_file).and_return(true)
289
+
290
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
291
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
292
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
293
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
294
+
295
+ expect(receive_transaction.check_complete).to be true
296
+ expect(receive_transaction.instance_variable_get(:@file_status)).to eq("FILESTORE_SUCCESS")
297
+ expect(receive_transaction.instance_variable_get(:@delivery_code)).to eq("DATA_COMPLETE")
298
+ end
299
+
300
+ it "handles filestore rejection" do
301
+ allow(CfdpMib).to receive(:put_destination_file).and_return(false)
302
+
303
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
304
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
305
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
306
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
307
+
308
+ expect(receive_transaction.check_complete).to be true
309
+ expect(receive_transaction.instance_variable_get(:@file_status)).to eq("FILESTORE_REJECTION")
310
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("FILESTORE_REJECTION")
311
+ end
312
+
313
+ it "handles checksum failure" do
314
+ allow(@mock_null_checksum).to receive(:check).and_return(false)
315
+
316
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
317
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
318
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
319
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
320
+
321
+ expect(receive_transaction.check_complete).to be true
322
+ expect(receive_transaction.instance_variable_get(:@file_status)).to eq("FILE_DISCARDED")
323
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("FILE_CHECKSUM_FAILURE")
324
+ expect(receive_transaction.instance_variable_get(:@delivery_code)).to eq("DATA_INCOMPLETE")
325
+ end
326
+
327
+ it "processes filestore requests" do
328
+ metadata_with_tlv = @metadata_pdu_hash.dup
329
+ metadata_with_tlv["TLVS"] = [
330
+ {
331
+ "TYPE" => "FILESTORE_REQUEST",
332
+ "ACTION_CODE" => "CREATE_FILE",
333
+ "FIRST_FILE_NAME" => "create.txt"
334
+ }
335
+ ]
336
+
337
+ allow(CfdpMib).to receive(:filestore_request).and_return(["SUCCESSFUL", "File created"])
338
+ allow(CfdpMib).to receive(:put_destination_file).and_return(true)
339
+
340
+ receive_transaction = CfdpReceiveTransaction.new(metadata_with_tlv)
341
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
342
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
343
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
344
+
345
+ expect(receive_transaction.check_complete).to be true
346
+ expect(receive_transaction.instance_variable_get(:@filestore_responses).length).to eq(1)
347
+ expect(receive_transaction.instance_variable_get(:@filestore_responses)[0]["STATUS_CODE"]).to eq("SUCCESSFUL")
348
+ end
349
+ end
350
+
351
+ describe "complete_file_received?" do
352
+ it "returns false if file_size is not set" do
353
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
354
+ receive_transaction.instance_variable_set(:@file_size, nil)
355
+ expect(receive_transaction.complete_file_received?).to be false
356
+ end
357
+
358
+ it "returns true when all segments are received" do
359
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
360
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
361
+ receive_transaction.handle_pdu(@file_data_pdu_hash2)
362
+ expect(receive_transaction.complete_file_received?).to be true
363
+ end
364
+
365
+ it "returns false when segments are missing" do
366
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
367
+ receive_transaction.handle_pdu(@file_data_pdu_hash)
368
+ # Skip the second segment
369
+ expect(receive_transaction.complete_file_received?).to be false
370
+ end
371
+
372
+ it "returns true when segments cover entire file size even if not starting at 0" do
373
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
374
+ # Create an overlapping segment case - not starting at 0
375
+ receive_transaction.instance_variable_set(:@segments, {5 => 100})
376
+ expect(receive_transaction.complete_file_received?).to be false
377
+
378
+ # Now add a segment that covers 0-5
379
+ receive_transaction.instance_variable_set(:@segments, {0 => 5, 5 => 100})
380
+ expect(receive_transaction.complete_file_received?).to be true
381
+ end
382
+ end
383
+
384
+ describe "cancel" do
385
+ it "cancels the transaction" do
386
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
387
+ receive_transaction.cancel
388
+ expect(receive_transaction.instance_variable_get(:@state)).to eq("CANCELED")
389
+ expect(receive_transaction.instance_variable_get(:@transaction_status)).to eq("TERMINATED")
390
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("CANCEL_REQUEST_RECEIVED")
391
+ end
392
+ end
393
+
394
+ describe "suspend" do
395
+ it "suspends acknowledged transactions" do
396
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
397
+ receive_transaction.suspend
398
+ expect(receive_transaction.instance_variable_get(:@state)).to eq("SUSPENDED")
399
+ end
400
+ end
401
+
402
+ describe "update" do
403
+ it "updates check timeouts" do
404
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
405
+ receive_transaction.instance_variable_set(:@check_timeout, Time.now - 1)
406
+ expect(receive_transaction).to receive(:handle_fault)
407
+
408
+ # Set count to reach limit
409
+ receive_transaction.instance_variable_set(:@check_timeout_count, @source_entity['check_limit'] - 1)
410
+ receive_transaction.update
411
+
412
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("CHECK_LIMIT_REACHED")
413
+ expect(receive_transaction.instance_variable_get(:@check_timeout)).to be_nil
414
+ end
415
+
416
+ it "sends NAKs when nak_timeout expires" do
417
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
418
+ receive_transaction.instance_variable_set(:@nak_timeout, Time.now - 1)
419
+ expect(receive_transaction).to receive(:send_naks).with(true)
420
+
421
+ receive_transaction.update
422
+
423
+ expect(receive_transaction.instance_variable_get(:@nak_timeout_count)).to eq(1)
424
+ end
425
+
426
+ it "handles nak_timeout expiration limit" do
427
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
428
+ receive_transaction.instance_variable_set(:@nak_timeout, Time.now - 1)
429
+ expect(receive_transaction).to receive(:send_naks).with(true)
430
+ expect(receive_transaction).to receive(:handle_fault)
431
+
432
+ # Set count to reach limit
433
+ receive_transaction.instance_variable_set(:@nak_timeout_count, @source_entity['nak_timer_expiration_limit'] - 1)
434
+ receive_transaction.update
435
+
436
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("NAK_LIMIT_REACHED")
437
+ expect(receive_transaction.instance_variable_get(:@nak_timeout)).to be_nil
438
+ end
439
+
440
+ it "sends keep alive when keep_alive_timeout expires" do
441
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
442
+ receive_transaction.instance_variable_set(:@keep_alive_timeout, Time.now - 1)
443
+ expect(receive_transaction).to receive(:send_keep_alive)
444
+
445
+ receive_transaction.update
446
+
447
+ expect(receive_transaction.instance_variable_get(:@keep_alive_count)).to eq(1)
448
+ end
449
+
450
+ it "clears keep_alive_timeout when EOF received" do
451
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
452
+ receive_transaction.instance_variable_set(:@keep_alive_timeout, Time.now + 10)
453
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
454
+
455
+ receive_transaction.update
456
+
457
+ expect(receive_transaction.instance_variable_get(:@keep_alive_timeout)).to be_nil
458
+ end
459
+
460
+ it "handles inactivity timeout" do
461
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
462
+ receive_transaction.instance_variable_set(:@inactivity_timeout, Time.now - 1)
463
+
464
+ receive_transaction.update
465
+
466
+ expect(receive_transaction.instance_variable_get(:@inactivity_count)).to eq(1)
467
+ end
468
+
469
+ it "handles inactivity limit reached" do
470
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
471
+ receive_transaction.instance_variable_set(:@inactivity_timeout, Time.now - 1)
472
+ expect(receive_transaction).to receive(:handle_fault)
473
+
474
+ # Set count to reach limit
475
+ receive_transaction.instance_variable_set(:@inactivity_count, @source_entity['transaction_inactivity_limit'] - 1)
476
+ receive_transaction.update
477
+
478
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("INACTIVITY_DETECTED")
479
+ end
480
+
481
+ it "clears inactivity_timeout when EOF received" do
482
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
483
+ receive_transaction.instance_variable_set(:@inactivity_timeout, Time.now + 10)
484
+ receive_transaction.instance_variable_set(:@eof_pdu_hash, @eof_pdu_hash)
485
+
486
+ receive_transaction.update
487
+
488
+ expect(receive_transaction.instance_variable_get(:@inactivity_timeout)).to be_nil
489
+ end
490
+
491
+ it "resends finished PDU when finished_ack_timeout expires" do
492
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
493
+ receive_transaction.instance_variable_set(:@finished_ack_timeout, Time.now - 1)
494
+ receive_transaction.instance_variable_set(:@finished_pdu, "FINISHED_PDU")
495
+
496
+ receive_transaction.update
497
+
498
+ expect(receive_transaction.instance_variable_get(:@finished_count)).to eq(1)
499
+ end
500
+
501
+ it "handles finished_ack limit reached" do
502
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
503
+ receive_transaction.instance_variable_set(:@finished_ack_timeout, Time.now - 1)
504
+ receive_transaction.instance_variable_set(:@finished_pdu, "FINISHED_PDU")
505
+ expect(receive_transaction).to receive(:handle_fault)
506
+
507
+ # Set count to reach limit
508
+ receive_transaction.instance_variable_set(:@finished_count, @source_entity['ack_timer_expiration_limit'])
509
+ receive_transaction.update
510
+
511
+ expect(receive_transaction.instance_variable_get(:@condition_code)).to eq("ACK_LIMIT_REACHED")
512
+ expect(receive_transaction.instance_variable_get(:@finished_ack_timeout)).to be_nil
513
+ end
514
+ end
515
+
516
+ describe "send_keep_alive" do
517
+ it "sends a keep alive PDU" do
518
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
519
+ expect(CfdpPdu).to receive(:build_keep_alive_pdu)
520
+
521
+ receive_transaction.send_keep_alive
522
+ end
523
+ end
524
+
525
+ describe "send_naks" do
526
+ before(:each) do
527
+ # For these tests we'll skip the actual implementation since it requires more dependencies
528
+ allow_any_instance_of(CfdpReceiveTransaction).to receive(:send_naks).and_return(nil)
529
+ end
530
+
531
+ it "sends NAK PDUs for missing segments" do
532
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
533
+ receive_transaction.instance_variable_set(:@file_size, 100)
534
+ receive_transaction.instance_variable_set(:@segments, {0 => 50})
535
+ receive_transaction.instance_variable_set(:@progress, 50)
536
+
537
+ # Just verify the method is called
538
+ expect(receive_transaction).to receive(:send_naks)
539
+ receive_transaction.send_naks
540
+ end
541
+
542
+ it "handles forced NAK sending" do
543
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
544
+ receive_transaction.instance_variable_set(:@file_size, 100)
545
+ receive_transaction.instance_variable_set(:@segments, {0 => 100})
546
+ receive_transaction.instance_variable_set(:@progress, 100)
547
+
548
+ # Just verify the different call signatures
549
+ expect(receive_transaction).to receive(:send_naks).with(no_args)
550
+ receive_transaction.send_naks
551
+
552
+ expect(receive_transaction).to receive(:send_naks).with(true)
553
+ receive_transaction.send_naks(true)
554
+ end
555
+
556
+ it "handles large file segments properly" do
557
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
558
+ receive_transaction.instance_variable_set(:@file_size, 200)
559
+ # Create a gap from 50-150
560
+ receive_transaction.instance_variable_set(:@segments, {0 => 50, 150 => 200})
561
+ receive_transaction.instance_variable_set(:@progress, 100)
562
+
563
+ # Just verify the method is called
564
+ expect(receive_transaction).to receive(:send_naks)
565
+ receive_transaction.send_naks
566
+ end
567
+ end
568
+
569
+ describe "notice_of_completion" do
570
+ it "sends a finished PDU" do
571
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
572
+ expect(CfdpPdu).to receive(:build_finished_pdu)
573
+
574
+ receive_transaction.notice_of_completion
575
+
576
+ expect(receive_transaction.instance_variable_get(:@state)).to eq("FINISHED")
577
+ expect(receive_transaction.instance_variable_get(:@transaction_status)).to eq("TERMINATED")
578
+ end
579
+
580
+ it "handles transaction finished indication" do
581
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
582
+ expect(CfdpPdu).to receive(:build_finished_pdu)
583
+ expect(CfdpTopic).to receive(:write_indication).with("Transaction-Finished", any_args)
584
+
585
+ receive_transaction.notice_of_completion
586
+ end
587
+
588
+ it "doesn't send finished PDU when not enabled" do
589
+ @source_entity['enable_finished'] = false
590
+ allow(CfdpMib).to receive(:entity).with(1).and_return(@source_entity)
591
+
592
+ receive_transaction = CfdpReceiveTransaction.new(@metadata_pdu_hash)
593
+ expect(CfdpPdu).not_to receive(:build_finished_pdu)
594
+
595
+ receive_transaction.notice_of_completion
596
+ end
597
+ end
598
+ end