data-dictionary.test.mjs 6.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import {readProjectDataDictionary,readAuthorizedDataDictionary} from '../src/data-dictionary.mjs';
  5. import {describeBusinessSchema} from '../../skills/ksf-workshop-runtime/scripts/describe-business-schema.mjs';
  6. import {runWorkshop} from '../../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  7. const dictionary=JSON.parse(fs.readFileSync(new URL('../references/business-data-dictionary.json',import.meta.url),'utf8'));
  8. const definitions=JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json',import.meta.url),'utf8'));
  9. test('完整字典逐表逐列覆盖实际维护定义,来源、系统字段与写入口分开',()=>{
  10. assert.deepEqual(Object.keys(dictionary.tables),Object.keys(definitions.tables));
  11. assert.equal(dictionary.fieldCount,1388);
  12. for(const [table,defs]of Object.entries(definitions.tables)){
  13. assert.deepEqual(Object.keys(dictionary.tables[table].fields),Object.keys(defs));
  14. for(const [name,def]of Object.entries(defs)){
  15. const field=dictionary.tables[table].fields[name];assert.ok(field.meaning);assert.equal(field.type,def.type);
  16. assert.equal(field.targetClass,def.targetClass);
  17. }
  18. }
  19. assert.equal(dictionary.tables.KsfServiceCredential.fields.tokenHash.allowedInGenericImport,false);
  20. assert.equal(dictionary.tables.KfProduct.fields.objectId.writeOwner,'service');
  21. });
  22. test('结构接口只查元信息,保留未知表列并拒绝SQL注入',async()=>{
  23. const queries=[];const pg={any:async(sql,args)=>{
  24. queries.push({sql,args});assert.ok(sql.includes('information_schema.'));assert.ok(!/SELECT\s+\*|INSERT|UPDATE|DELETE|ALTER/i.test(sql));
  25. return sql.includes('information_schema.columns')?[
  26. {table_name:'KfProduct',column_name:'name',data_type:'text',udt_name:'text',is_nullable:'YES',has_default:false},
  27. {table_name:'KfProduct',column_name:'newField',data_type:'text',is_nullable:'YES'},
  28. {table_name:'KfExtra',column_name:'newColumn',data_type:'text',is_nullable:'YES'},
  29. {table_name:'KsfServiceCredential',column_name:'tokenHash',data_type:'text',is_nullable:'YES'}]:[];
  30. }};
  31. const result=await readProjectDataDictionary(pg,dictionary);
  32. assert.equal(result.databaseWrites,0);assert.equal(result.rowValuesReturned,false);assert.equal(result.tableCount,3);
  33. assert.equal(result.semanticCoverageComplete,false);assert.deepEqual(result.unknownFields,['KfProduct.newField','KfExtra.newColumn']);
  34. assert.ok(!JSON.stringify(result).includes('secret-value'));
  35. await assert.rejects(()=>readProjectDataDictionary(pg,dictionary,{table:'KfProduct;DROP TABLE x'}),{code:'VALIDATION_FAILED'});
  36. assert.equal(queries.length,2);
  37. const compared=describeBusinessSchema(dictionary,{live:result});assert.equal(compared.live.readyForFieldMapping,false);
  38. });
  39. test('workshop-schema只发结构查询,不创建身份或业务记录',async()=>{
  40. const calls=[];const r=await runWorkshop('workshop-schema',{summary:true},{requestId:'schema-1',call:async(name,p)=>{
  41. calls.push({name,p});return{readOnly:true,databaseWrites:0,tables:[]};
  42. }});
  43. assert.equal(r.databaseWrites,0);assert.deepEqual(calls,[{name:'ksfGetDataDictionary',p:{summary:true,requestId:'schema-1'}}]);
  44. });
  45. test('匿名与缺data:read在读取实际表结构前停止,不因字典扩大行数据权限',async()=>{
  46. let reads=0;
  47. const pg={any:async()=>{reads++;return[];}};
  48. const error=code=>{throw Object.assign(Error(code),{code});};
  49. const app={authenticate:async()=>null,need:actor=>{if(!actor)error('AUTH_REQUIRED');if(!actor.scopes.includes('data:read'))error('FORBIDDEN');},fail:error};
  50. await assert.rejects(()=>readAuthorizedDataDictionary({app,pg,dictionary},{},{}),{code:'AUTH_REQUIRED'});
  51. app.authenticate=async()=>({scopes:['ingest:write']});
  52. await assert.rejects(()=>readAuthorizedDataDictionary({app,pg,dictionary},{},{}),{code:'FORBIDDEN'});
  53. assert.equal(reads,0);
  54. });
  55. test('真实列类型漂移可识别,摘要回执不能冒充逐字段验证',()=>{
  56. const live={schema:'ksf.live-business-data-dictionary.v1',readOnly:true,databaseWrites:0,
  57. semanticCoverageComplete:true,unknownFields:[],tableCount:1,fieldCount:1,
  58. tables:[{name:'KfProduct',fields:[{name:'name',logicalName:'name',physicalType:'boolean'}]}]};
  59. const r=describeBusinessSchema(dictionary,{live});
  60. assert.equal(r.live.readyForFieldMapping,false);
  61. assert.ok(r.live.differences.some(x=>x.reason==='TYPE_MISMATCH'&&x.field==='name'));
  62. assert.throws(()=>describeBusinessSchema(dictionary,{live:{...live,tables:[{name:'KfProduct',fieldCount:1}]}}),/LIVE_DETAILED_SCHEMA_RECEIPT_REQUIRED/);
  63. });
  64. test('独立生产结构快照全部1451列有说明,账号扩展仅列元信息并保留平台用途边界',async()=>{
  65. const snapshot=dictionary.physicalSchemaSnapshot;
  66. assert.equal(snapshot.tableCount,50);assert.equal(snapshot.fieldCount,1451);assert.equal(snapshot.rowValuesReturned,false);
  67. assert.equal(snapshot.tables.reduce((n,t)=>n+t.fields.length,0),1451);
  68. for(const t of snapshot.tables)for(const f of t.fields)assert.ok(f.meaning,t.name+'.'+f.name);
  69. const extra=dictionary.platformFields._User;
  70. assert.equal(Object.keys(extra).length,6);
  71. for(const f of Object.values(extra))assert.equal(f.allowedInGenericImport,false);
  72. assert.equal(extra.hash.purposeStatus,'platform_reserved');
  73. const pg={any:async sql=>sql.includes('information_schema.columns')?snapshot.tables.flatMap(t=>t.fields.map(f=>({table_name:t.name,column_name:f.name,data_type:f.physicalType,udt_name:f.userType,is_nullable:f.nullable?'YES':'NO',has_default:f.hasDefault}))):[]};
  74. const actual=await readProjectDataDictionary(pg,dictionary);
  75. assert.deepEqual(actual.unknownFields,[]);assert.equal(actual.semanticCoverageComplete,true);
  76. assert.equal(actual.fieldCount,1451);assert.equal(actual.rowValuesReturned,false);
  77. const compared=describeBusinessSchema(dictionary,{live:actual});
  78. assert.equal(compared.live.tablesReadyForFieldMapping.KfProduct,true);
  79. assert.ok(!compared.live.differences.some(x=>x.field==='ACL'||x.field==='password'||dictionary.tables[x.table]?.fields[x.field]?.type==='Relation'));
  80. assert.equal(dictionary.tables.KfConcept.fields.insightRefs.allowedInGenericImport,false);
  81. });
  82. test('登记的Relation关系表纳入实际结构,ACL和密码的物理存储不误报缺列',async()=>{
  83. const queries=[];const pg={any:async(sql,args)=>{queries.push(args);return sql.includes('information_schema.columns')?[
  84. {table_name:'_Join:insightRefs:KfConcept',column_name:'owningId',data_type:'text',is_nullable:'NO'},
  85. {table_name:'_Join:insightRefs:KfConcept',column_name:'relatedId',data_type:'text',is_nullable:'NO'}]:[];}};
  86. const r=await readProjectDataDictionary(pg,dictionary,{table:'_Join:insightRefs:KfConcept'});
  87. assert.equal(r.relationTableCount,1);assert.deepEqual(r.unknownFields,[]);assert.ok(queries[0][1].includes('_Join:insightRefs:KfConcept'));
  88. assert.match(r.tables[0].fields[1].meaning,/KfInsight/);
  89. await assert.rejects(()=>readProjectDataDictionary(pg,dictionary,{table:'_Join:secret:OtherApp'}),{code:'VALIDATION_FAILED'});
  90. });