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,331 @@
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
+
20
+ RSpec.describe CfdpTransaction do
21
+ before(:each) do
22
+ # Mock CfdpTopic
23
+ allow(CfdpTopic).to receive(:write_indication)
24
+
25
+ # Mock CfdpMib
26
+ @source_entity = {
27
+ 'id' => 1,
28
+ 'name' => 'SOURCE',
29
+ 'fault_handler' => {
30
+ 'NO_ERROR' => 'IGNORE_ERROR',
31
+ 'FILESTORE_REJECTION' => 'ISSUE_NOTICE_OF_CANCELLATION',
32
+ 'FILE_CHECKSUM_FAILURE' => 'ISSUE_NOTICE_OF_SUSPENSION',
33
+ 'FILE_SIZE_ERROR' => 'ABANDON_TRANSACTION',
34
+ 'CHECK_LIMIT_REACHED' => 'IGNORE_ERROR'
35
+ },
36
+ 'suspended_indication' => true,
37
+ 'resume_indication' => true,
38
+ 'keep_alive_interval' => 5
39
+ }
40
+
41
+ allow(CfdpMib).to receive(:source_entity).and_return(@source_entity)
42
+
43
+ # Mock logging
44
+ allow(OpenC3::Logger).to receive(:info)
45
+ allow(OpenC3::Logger).to receive(:error)
46
+
47
+ # Mock Api module
48
+ allow_any_instance_of(CfdpTransaction).to receive(:cmd)
49
+
50
+ ENV['OPENC3_SCOPE'] = 'DEFAULT'
51
+ end
52
+
53
+ describe "initialize" do
54
+ it "initializes with default values" do
55
+ transaction = CfdpTransaction.new
56
+
57
+ expect(transaction.frozen).to be false
58
+ expect(transaction.state).to eq("ACTIVE")
59
+ expect(transaction.transaction_status).to eq("ACTIVE")
60
+ expect(transaction.progress).to eq(0)
61
+ expect(transaction.condition_code).to eq("NO_ERROR")
62
+ expect(transaction.delivery_code).to be_nil
63
+ expect(transaction.instance_variable_get(:@metadata_pdu_hash)).to be_nil
64
+ expect(transaction.instance_variable_get(:@metadata_pdu_count)).to eq(0)
65
+ expect(transaction.proxy_response_info).to be_nil
66
+ expect(transaction.proxy_response_needed).to be false
67
+ expect(transaction.instance_variable_get(:@source_file_name)).to be_nil
68
+ expect(transaction.instance_variable_get(:@destination_file_name)).to be_nil
69
+ expect(transaction.create_time).to be_a(Time)
70
+ expect(transaction.complete_time).to be_nil
71
+ end
72
+ end
73
+
74
+ describe "class methods" do
75
+ it "builds a transaction id" do
76
+ id = CfdpTransaction.build_transaction_id(1, 123)
77
+ expect(id).to eq("1__123")
78
+ end
79
+ end
80
+
81
+ describe "instance methods" do
82
+ let(:transaction) { CfdpTransaction.new }
83
+
84
+ describe "suspend" do
85
+ it "suspends an active transaction" do
86
+ transaction.suspend
87
+
88
+ expect(transaction.state).to eq("SUSPENDED")
89
+ expect(transaction.condition_code).to eq("SUSPEND_REQUEST_RECEIVED")
90
+ expect(CfdpTopic).to have_received(:write_indication).with("Suspended", hash_including(transaction_id: nil, condition_code: "SUSPEND_REQUEST_RECEIVED"))
91
+ end
92
+
93
+ it "does nothing if transaction is not active" do
94
+ transaction.instance_variable_set(:@state, "FINISHED")
95
+ transaction.suspend
96
+
97
+ expect(transaction.state).to eq("FINISHED")
98
+ end
99
+ end
100
+
101
+ describe "resume" do
102
+ it "resumes a suspended transaction" do
103
+ transaction.instance_variable_set(:@state, "SUSPENDED")
104
+ transaction.resume
105
+
106
+ expect(transaction.state).to eq("ACTIVE")
107
+ expect(transaction.condition_code).to eq("NO_ERROR")
108
+ expect(CfdpTopic).to have_received(:write_indication).with("Resumed", hash_including(transaction_id: nil, progress: 0))
109
+ end
110
+
111
+ it "does nothing if transaction is not suspended" do
112
+ transaction.resume
113
+
114
+ expect(transaction.state).to eq("ACTIVE")
115
+ expect(CfdpTopic).not_to have_received(:write_indication).with("Resumed", any_args)
116
+ end
117
+ end
118
+
119
+ describe "cancel" do
120
+ it "cancels an active transaction" do
121
+ transaction.cancel
122
+
123
+ expect(transaction.state).to eq("CANCELED")
124
+ expect(transaction.transaction_status).to eq("TERMINATED")
125
+ expect(transaction.condition_code).to eq("CANCEL_REQUEST_RECEIVED")
126
+ expect(transaction.complete_time).to be_a(Time)
127
+ end
128
+
129
+ it "cancels with a canceling entity id" do
130
+ transaction.cancel(3)
131
+
132
+ expect(transaction.state).to eq("CANCELED")
133
+ expect(transaction.instance_variable_get(:@canceling_entity_id)).to eq(3)
134
+ end
135
+
136
+ it "does nothing if transaction is already finished" do
137
+ transaction.instance_variable_set(:@state, "FINISHED")
138
+ original_time = Time.now.utc - 10
139
+ transaction.instance_variable_set(:@complete_time, original_time)
140
+
141
+ transaction.cancel
142
+
143
+ expect(transaction.state).to eq("FINISHED")
144
+ expect(transaction.complete_time).to eq(original_time)
145
+ end
146
+ end
147
+
148
+ describe "abandon" do
149
+ it "abandons an active transaction" do
150
+ transaction.abandon
151
+
152
+ expect(transaction.state).to eq("ABANDONED")
153
+ expect(transaction.transaction_status).to eq("TERMINATED")
154
+ expect(CfdpTopic).to have_received(:write_indication).with("Abandoned", hash_including(transaction_id: nil, condition_code: "NO_ERROR", progress: 0))
155
+ expect(transaction.complete_time).to be_a(Time)
156
+ end
157
+
158
+ it "does nothing if transaction is already finished" do
159
+ transaction.instance_variable_set(:@state, "FINISHED")
160
+ original_time = Time.now.utc - 10
161
+ transaction.instance_variable_set(:@complete_time, original_time)
162
+
163
+ transaction.abandon
164
+
165
+ expect(transaction.state).to eq("FINISHED")
166
+ expect(transaction.complete_time).to eq(original_time)
167
+ end
168
+ end
169
+
170
+ describe "report" do
171
+ it "sends a report indication" do
172
+ transaction.report
173
+
174
+ expect(CfdpTopic).to have_received(:write_indication).with("Report", hash_including(transaction_id: nil))
175
+ end
176
+ end
177
+
178
+ describe "freeze and unfreeze" do
179
+ it "freezes the transaction" do
180
+ transaction.freeze
181
+ expect(transaction.instance_variable_get(:@freeze)).to be true
182
+ end
183
+
184
+ it "unfreezes the transaction" do
185
+ transaction.instance_variable_set(:@freeze, true)
186
+ transaction.unfreeze
187
+ expect(transaction.instance_variable_get(:@freeze)).to be false
188
+ end
189
+ end
190
+
191
+ describe "build_report" do
192
+ it "generates a JSON report" do
193
+ transaction.instance_variable_set(:@id, "1__123")
194
+ report = transaction.build_report
195
+
196
+ expect(report).to be_a(String)
197
+
198
+ # Parse and verify JSON structure
199
+ json = JSON.parse(report)
200
+ expect(json["id"]).to eq("1__123")
201
+ expect(json["state"]).to eq("ACTIVE")
202
+ expect(json["transaction_status"]).to eq("ACTIVE")
203
+ expect(json["progress"]).to eq(0)
204
+ expect(json["frozen"]).to be false
205
+ end
206
+ end
207
+
208
+ describe "as_json" do
209
+ it "returns a hash representation of the transaction" do
210
+ transaction.instance_variable_set(:@id, "1__123")
211
+ transaction.instance_variable_set(:@source_file_name, "source.txt")
212
+ transaction.instance_variable_set(:@destination_file_name, "dest.txt")
213
+
214
+ json = transaction.as_json
215
+
216
+ expect(json).to be_a(Hash)
217
+ expect(json["id"]).to eq("1__123")
218
+ expect(json["state"]).to eq("ACTIVE")
219
+ expect(json["source_file_name"]).to eq("source.txt")
220
+ expect(json["destination_file_name"]).to eq("dest.txt")
221
+ expect(json["create_time"]).to be_a(String)
222
+ expect(json["complete_time"]).to be_nil
223
+ end
224
+
225
+ it "includes complete_time when available" do
226
+ transaction.instance_variable_set(:@complete_time, Time.now.utc)
227
+
228
+ json = transaction.as_json
229
+
230
+ expect(json["complete_time"]).to be_a(String)
231
+ end
232
+ end
233
+
234
+ describe "handle_fault" do
235
+ it "handles ISSUE_NOTICE_OF_CANCELLATION response" do
236
+ transaction.instance_variable_set(:@condition_code, "FILESTORE_REJECTION")
237
+
238
+ expect(transaction).to receive(:cancel)
239
+ transaction.handle_fault
240
+ end
241
+
242
+ it "handles ISSUE_NOTICE_OF_SUSPENSION response" do
243
+ transaction.instance_variable_set(:@condition_code, "FILE_CHECKSUM_FAILURE")
244
+
245
+ expect(transaction).to receive(:suspend)
246
+ transaction.handle_fault
247
+ end
248
+
249
+ it "handles ABANDON_TRANSACTION response" do
250
+ transaction.instance_variable_set(:@condition_code, "FILE_SIZE_ERROR")
251
+
252
+ expect(transaction).to receive(:abandon)
253
+ transaction.handle_fault
254
+ end
255
+
256
+ it "handles IGNORE_ERROR response" do
257
+ transaction.instance_variable_set(:@condition_code, "CHECK_LIMIT_REACHED")
258
+
259
+ expect(transaction).to receive(:ignore_fault)
260
+ transaction.handle_fault
261
+ end
262
+
263
+ it "uses fault handler overrides" do
264
+ transaction.instance_variable_set(:@condition_code, "CHECK_LIMIT_REACHED")
265
+ transaction.instance_variable_set(:@fault_handler_overrides, {"CHECK_LIMIT_REACHED" => "ISSUE_NOTICE_OF_CANCELLATION"})
266
+
267
+ expect(transaction).to receive(:cancel)
268
+ transaction.handle_fault
269
+ end
270
+ end
271
+
272
+ describe "ignore_fault" do
273
+ it "sends a fault indication" do
274
+ transaction.instance_variable_set(:@condition_code, "FILE_SIZE_ERROR")
275
+
276
+ transaction.ignore_fault
277
+
278
+ expect(CfdpTopic).to have_received(:write_indication).with("Fault", hash_including(transaction_id: nil, condition_code: "FILE_SIZE_ERROR", progress: 0))
279
+ end
280
+ end
281
+
282
+ describe "get_checksum" do
283
+ it "returns a CfdpChecksum for type 0" do
284
+ checksum = transaction.get_checksum(0)
285
+ expect(checksum).to be_a(CfdpChecksum)
286
+ end
287
+
288
+ it "returns a CfdpCrcChecksum for type 1" do
289
+ checksum = transaction.get_checksum(1)
290
+ expect(checksum).to be_a(CfdpCrcChecksum)
291
+ end
292
+
293
+ it "returns a CfdpCrcChecksum for type 2" do
294
+ checksum = transaction.get_checksum(2)
295
+ expect(checksum).to be_a(CfdpCrcChecksum)
296
+ end
297
+
298
+ it "returns a CfdpCrcChecksum for type 3" do
299
+ checksum = transaction.get_checksum(3)
300
+ expect(checksum).to be_a(CfdpCrcChecksum)
301
+ end
302
+
303
+ it "returns a CfdpNullChecksum for type 15" do
304
+ checksum = transaction.get_checksum(15)
305
+ expect(checksum).to be_a(CfdpNullChecksum)
306
+ end
307
+
308
+ it "returns nil for unknown checksum types" do
309
+ checksum = transaction.get_checksum(10)
310
+ expect(checksum).to be_nil
311
+ end
312
+ end
313
+
314
+ describe "cfdp_cmd" do
315
+ it "sends a command with the correct parameters" do
316
+ entity = {'cmd_delay' => 0.1}
317
+
318
+ expect(transaction).to receive(:cmd).with('TARGET', 'PACKET', {'PARAM' => 'value'}, scope: 'DEFAULT')
319
+
320
+ transaction.cfdp_cmd(entity, 'TARGET', 'PACKET', {'PARAM' => 'value'}, scope: 'DEFAULT')
321
+ end
322
+ end
323
+
324
+ describe "update" do
325
+ it "does nothing in the base class" do
326
+ # Just verifying it doesn't raise an error
327
+ transaction.update
328
+ end
329
+ end
330
+ end
331
+ end