require_relative '../test_helper' require_relative '../../examples/demo_api' require 'open3' require 'fileutils' # # the examples of README.md section 2: the code in the documentation is the file, # and every example runs and produces the documented result # class DocExamplesTest < PipelineTest DOC = File.read(File.join(ROOT, 'README.md')) EXAMPLES = DOC.scan(/\n```ruby\n(.*?)\n```/m) def run_example dir, name, *args, env: {} env = { 'BUNDLE_GEMFILE' => File.join(ROOT, 'Gemfile') }.merge(env) Open3.capture3(env, RbConfig.ruby, File.join(ROOT, 'bin/etl'), 'run', File.join(ROOT, name), "--out=#{dir}", *args, chdir: dir) end def test_documented_code_equals_files assert_equal %w[examples/kunden_import.rb examples/rechnungen_satzarten.rb examples/bewegungen_inkrementell.rb examples/eigenes_plugin.rb examples/rest_lesen.rb examples/rest_schreiben.rb examples/rest_zu_rest.rb examples/buchungen_export.rb], EXAMPLES.map(&:first) EXAMPLES.each do |file, code| assert_equal File.read(File.join(ROOT, file)).rstrip, code.rstrip, "#{file} differs from README.md" end end def test_kunden_import with_tmpdir do |dir| out, err, status = run_example(dir, 'examples/kunden_import.rb') assert_equal 2, status.exitstatus, err assert_includes out, 'kunden_import: read=9 written=4 rejected=4' db = DataPipeline::SqliteStorage.new(database: "#{dir}/kunden.sqlite3", query: 'select * from kunden order by kundennr') rows = db.read db.close assert_equal %w[100001 100002 100003 100009], rows.map { |r| r['kundennr'] } assert_equal({ 'kundennr' => '100002', 'name' => 'Huber & Partner', 'land' => 'Österreich', 'eu' => 'ja', 'umsatz' => 8300.0 }, rows[1]) excel = DataPipeline::ExcelStorage.new(file: "#{dir}/eu_kunden.xlsx", sheet: 'EU-Kunden').read assert_equal %w[100001 100002 100009], excel.map { |r| r['kundennr'] } errors = DataPipeline::CsvStorage.new(file: "#{dir}/kunden_fehler.csv").read assert_equal ['name: missing', 'kundennr: format /\A\d{6}\z/', 'Land XX unbekannt', 'umsatz: not decimal'], errors.map { |r| r['_error'] } # a second run replaces the table content (truncate) run_example(dir, 'examples/kunden_import.rb') db = DataPipeline::SqliteStorage.new(database: "#{dir}/kunden.sqlite3", query: 'select count(*) as n from kunden') assert_equal 4, db.read.first['n'] db.close end end def test_kunden_import_dry_run_creates_no_files with_tmpdir do |dir| out, _err, status = run_example(dir, 'examples/kunden_import.rb', '--dry-run') assert_equal 2, status.exitstatus assert_includes out, 'DRY RUN' assert_empty Dir.children(dir) end end def test_rechnungen_satzarten with_tmpdir do |dir| _out, err, status = run_example(dir, 'examples/rechnungen_satzarten.rb') assert status.success?, err assert_includes err, 'TEXT: longer than 30' documented = DOC[/Ergebnis `tmp\/rechnungen.txt`.*?```text\n(.*?)```/m, 1] expected = documented.sub(/\ADATEI\|\d{8}/, "DATEI|#{Date.today.strftime('%Y%m%d')}") assert_equal expected, File.read("#{dir}/rechnungen.txt") assert_equal 3, File.readlines("#{dir}/positionen.ndjson").size end end def test_bewegungen_inkrementell with_tmpdir do |dir| out, err, status = run_example(dir, 'examples/bewegungen_inkrementell.rb') assert status.success?, err assert_includes out, 'neu: 4, Stand jetzt: 4' file = Dir["#{dir}/bewegungen_*.ndjson"].first assert_equal({ 'id' => 2, 'konto' => '4000', 'betrag' => 20.0, 'soll_haben' => 'H' }, JSON.parse(File.readlines(file)[1])) FileUtils.rm(file) out, = run_example(dir, 'examples/bewegungen_inkrementell.rb') assert_includes out, 'neu: 0, Stand jetzt: 4' assert_empty Dir["#{dir}/bewegungen_*.ndjson"], 'no file without new rows' end end def test_eigenes_plugin with_tmpdir do |dir| _out, err, status = run_example(dir, 'examples/eigenes_plugin.rb') assert status.success?, err documented = DOC[/Ergebnis `tmp\/alle_filialen.csv`:\n\n```text\n(.*?)```/m, 1] assert_equal documented, File.read("#{dir}/alle_filialen.csv") end end def with_demo_api api = DemoApi.start(port: 0) yield api ensure api&.stop end def test_rest_lesen with_demo_api do |api| with_tmpdir do |dir| out, err, status = run_example(dir, 'examples/rest_lesen.rb', "--url=#{api.url}") assert_equal 2, status.exitstatus, err assert_includes out, 'rest_lesen: read=7 written=6 rejected=1' documented = DOC[/Ergebnis `tmp\/api_kunden.csv`:\n\n```text\n(.*?)```/m, 1] assert_equal documented, File.read("#{dir}/api_kunden.csv") assert_equal 6, JSON.parse(File.read("#{dir}/api_kunden.json")).size assert_equal ['umsatz: not decimal'], DataPipeline::CsvStorage.new(file: "#{dir}/api_fehler.csv").read.map { |r| r['_error'] } end end end def test_rest_schreiben_and_dry_run with_demo_api do |api| with_tmpdir do |dir| out, _err, status = run_example(dir, 'examples/rest_schreiben.rb', "--url=#{api.url}", '--dry-run') assert_equal 2, status.exitstatus assert_includes out, 'DRY RUN' assert_empty api.imported, 'dry run sends nothing' out, err, status = run_example(dir, 'examples/rest_schreiben.rb', "--url=#{api.url}") assert_equal 2, status.exitstatus, err assert_includes out, 'gesendet: 6 Kunden in 3 Paketen' assert_equal %w[100001 100002 100003 100007 100008 100009], api.imported.map { |r| r['kundennr'] } assert_equal 2, DataPipeline::CsvStorage.new(file: "#{dir}/import_fehler.csv").read.size end end end def test_rest_zu_rest with_demo_api do |api| with_tmpdir do |dir| out, err, status = run_example(dir, 'examples/rest_zu_rest.rb', "--url=#{api.url}") assert status.success?, err assert_includes out, 'archiviert: 5, Stand jetzt: 5' assert_equal %w[1 2 3 4 5], api.archive.keys assert_equal Date.today.iso8601, api.archive['3']['archiviert'] api.archive.clear out, = run_example(dir, 'examples/rest_zu_rest.rb', "--url=#{api.url}") assert_includes out, 'archiviert: 0, Stand jetzt: 5' assert_empty api.archive end end end def test_buchungen_export with_tmpdir do |dir| env = { 'BUCHHALTUNG_DB' => "#{dir}/buchhaltung.sqlite3", 'SLACK_TOKEN' => nil } out, err, status = run_example(dir, 'examples/buchungen_export.rb', '--dry-run', env:) assert_equal 0, status.exitstatus, err refute File.exist?("#{dir}/buchhaltung.sqlite3") 2.times do out, err, status = run_example(dir, 'examples/buchungen_export.rb', env:) assert_equal 0, status.exitstatus, err end assert_includes out, 'buchungen_export: read=7 written=7 rejected=0 betrag=0/0' db = DataPipeline::SqliteStorage.new(database: "#{dir}/buchhaltung.sqlite3", query: 'select * from buchungen order by belnr, buzei') rows = db.read db.close assert_equal 7, rows.size # upsert: the second run updates instead of inserting assert_equal({ 'belnr' => '5100000002', 'buzei' => 3, 'datum' => '2026-10-02', 'konto' => '2000', 'shkzg' => 'H', 'dmbtr' => '409.80', 'betrag' => -409.8, 'buchungstext' => 'Papeterie Zürcher AG' }, rows[4]) assert_equal 'ohne Text', rows[5]['buchungstext'] csv = File.read("#{dir}/buchungen_sap.csv", encoding: 'Windows-1252').encode('UTF-8') assert_includes csv, "5100000001;1;2026-10-01;1020;S;1250.00;Zahlung Müller GmbH\n" history = DataPipeline.history("#{dir}/runs.ndjson", name: 'buchungen_export') assert_equal [true, true, true], history.map { |run| run['ok'] } assert_equal [true, false, false], history.map { |run| run['dry_run'] } end end end