Files
TS-CUCUMBER-WDIO-QK/test/unit/qkforce-test-data.test.mjs
isopropilick 896c3a121a
Some checks failed
CI / validate (push) Has been cancelled
ok
2026-08-27 15:00:50 -06:00

24 lines
6.1 KiB
JavaScript

import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { CONTENT_TYPE, loadQkforceTestData, parseRfc4180Csv, validateQkforceTestDataLock } from '../support/qkforce-test-data.mjs';
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
function workspace(csv, overrides={}) { const directory=fs.mkdtempSync(path.join(os.tmpdir(),'qkforce-test-data-')); fs.mkdirSync(path.join(directory,'test-data')); fs.writeFileSync(path.join(directory,'test-data','customers.csv'),csv); const lock={schemaVersion:1,datasets:[{datasetId:'tds_customers',versionId:'tdv_customers_1',versionNumber:1,logicalName:'customers',path:'test-data/customers.csv',sha256:digest(Buffer.from(csv)),rowCount:2,contentType:CONTENT_TYPE,...overrides}]}; fs.writeFileSync(path.join(directory,'qkforce.test-data.lock.json'),JSON.stringify(lock)); return {directory,lock}; }
const cleanup = directory => fs.rmSync(directory,{recursive:true,force:true});
const fixtureRoot=fileURLToPath(new URL('../fixtures/test-data/positive/',import.meta.url));
const negativeFixture=fileURLToPath(new URL('../fixtures/test-data/negative/unsafe-path.lock.json',import.meta.url));
test('starter positive and negative lock fixtures exercise the provider contract',()=>{ const provider=loadQkforceTestData(fixtureRoot,{required:true}); assert.equal(provider.dataset('customers').first({id:'1'}).name,'María, QA'); const unsafe=JSON.parse(fs.readFileSync(negativeFixture,'utf8')); assert.throws(()=>validateQkforceTestDataLock(unsafe),/safe package-local CSV path/); });
test('parses RFC-4180 quoted/multiline values, Unicode and empty cells deterministically',()=>{ const csv='id,name,note,empty\r\n1,"María, QA","line one\r\nline two",\r\n2,李,"quote ""inside""",\r\n'; const parsed=parseRfc4180Csv(csv,'customers'); assert.deepEqual(parsed.headers,['id','name','note','empty']); assert.equal(parsed.rows[0].note,'line one\r\nline two'); assert.equal(parsed.rows[1].note,'quote "inside"'); });
test('loads declared CSVs, verifies checksums and exposes result-safe provenance',()=>{ const csv='id,name,active,age\n1,María,true,30\n2,李,false,\n'; const {directory}=workspace(csv); try { const provider=loadQkforceTestData(directory,{required:true}); assert.deepEqual(provider.dataset('customers').row(1),{id:'2',name:'李',active:'false',age:''}); const provenance=provider.provenance(); assert.match(provenance.testDataLockSha256,/^[a-f0-9]{64}$/); assert.equal(JSON.stringify(provenance).includes('María'),false); } finally { cleanup(directory); } });
test('converts typed columns and explicit nullable empties',()=>{ const csv='id,active,age,score,birthday,note\n1,true,30,9.5,2026-08-26,\n2,false,,10,2024-02-29,plain\n'; const {directory}=workspace(csv); try { const dataset=loadQkforceTestData(directory).dataset('customers'); assert.deepEqual(dataset.typedRow(0,{id:'integer',active:'boolean',age:'integer',score:'number',birthday:'date',note:'string'}),{id:1,active:true,age:30,score:9.5,birthday:'2026-08-26',note:''}); assert.equal(dataset.typedRow(1,{age:{type:'integer',nullable:true}}).age,null); } finally { cleanup(directory); } });
test('type failures identify dataset/version and column without echoing sensitive values',()=>{ const csv='id,age\n1,SECRET-NOT-A-NUMBER\n2,30\n'; const {directory}=workspace(csv); try { const dataset=loadQkforceTestData(directory).dataset('customers'); assert.throws(()=>dataset.typedRow(0,{age:'integer'}),error=>{ assert.match(error.message,/datasetId=tds_customers, versionId=tdv_customers_1/); assert.match(error.message,/column "age"/); assert.doesNotMatch(error.message,/SECRET-NOT-A-NUMBER/); return true; }); } finally { cleanup(directory); } });
test('rejects path traversal, external paths and unsupported lock versions',()=>{ const valid={schemaVersion:1,datasets:[{datasetId:'tds_customers',versionId:'tdv_customers_1',versionNumber:1,logicalName:'customers',path:'test-data/customers.csv',sha256:'a'.repeat(64),rowCount:0,contentType:CONTENT_TYPE}]}; for (const unsafePath of ['../customers.csv','/tmp/customers.csv','test-data/../customers.csv','test-data/nested/customers.csv','test-data\\customers.csv']) { const candidate=structuredClone(valid); candidate.datasets[0].path=unsafePath; assert.throws(()=>validateQkforceTestDataLock(candidate),/safe package-local CSV path/); } const unsupported=structuredClone(valid); unsupported.schemaVersion=2; assert.throws(()=>validateQkforceTestDataLock(unsupported),/schemaVersion must be 1/); });
test('rejects duplicate headers, checksum mismatch, row-count mismatch and missing datasets',()=>{ assert.throws(()=>parseRfc4180Csv('id,id\n1,2\n','customers'),/duplicate header/); const csv='id,name\n1,A\n2,B\n'; const checksum=workspace(csv,{sha256:'0'.repeat(64)}); try { assert.throws(()=>loadQkforceTestData(checksum.directory),/checksum mismatch/); } finally { cleanup(checksum.directory); } const rowCount=workspace(csv,{rowCount:99}); try { assert.throws(()=>loadQkforceTestData(rowCount.directory),/row count mismatch/); } finally { cleanup(rowCount.directory); } const missing=workspace(csv); try { fs.rmSync(path.join(missing.directory,'test-data','customers.csv')); assert.throws(()=>loadQkforceTestData(missing.directory),/declared CSV file is missing/); } finally { cleanup(missing.directory); } });
test('no-data workspaces are valid and concurrent scenarios receive immutable rows',async()=>{ const emptyDirectory=fs.mkdtempSync(path.join(os.tmpdir(),'qkforce-no-data-')); try { const empty=loadQkforceTestData(emptyDirectory); assert.equal(empty.size,0); assert.deepEqual(empty.provenance(),{}); } finally { cleanup(emptyDirectory); } const csv='id,name\n1,A\n2,B\n'; const {directory}=workspace(csv); try { const provider=loadQkforceTestData(directory); const results=await Promise.all(Array.from({length:32},async(_,index)=>provider.dataset('customers').row(index%2))); assert.equal(results.filter(row=>row.id==='1').length,16); assert.equal(Object.isFrozen(results[0]),true); assert.throws(()=>{results[0].id='changed';},TypeError); } finally { cleanup(directory); } });