| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- <?php
- /** Configuration and simulated delivery tests; every fixture and audit write rolls back. */
- require dirname(__DIR__).'/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- date_default_timezone_set('Asia/Shanghai');
- use plugin\saiadmin\app\model\water\{WaterContent,WaterPackage,WaterSku,WaterLedger};
- use plugin\saiadmin\app\service\{WaterBusiness,WaterContentDashboard};
- use plugin\saiadmin\app\service\content\ContentPolicy;
- use plugin\saiadmin\app\controller\water\WaterController;
- use plugin\saiadmin\exception\ApiException;
- $checks=0;
- function check(bool $ok,string $label): void { global $checks; $checks++; if (!$ok) throw new RuntimeException($label); }
- function rejects(callable $fn,string $label): void { try {$fn();} catch (ApiException $e) {check(true,$label);return;} throw new RuntimeException($label); }
- function request(string $method,string $path,array $input=[]): \support\Request {
- $body=$method==='POST'?json_encode($input):'';
- if ($method==='GET' && $input) $path.='?'.http_build_query($input);
- return new \support\Request($method.' '.$path." HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ".strlen($body)."\r\n\r\n".$body);
- }
- $now=new DateTimeImmutable('2026-09-17 12:00:00',new DateTimeZone('Asia/Shanghai'));
- $service=new WaterContentDashboard($now); $business=new WaterBusiness(); $tag='CONTENT-'.bin2hex(random_bytes(4));
- $db=(new WaterContent())->getConnection();
- $before=WaterContent::query()->orderBy('id')->get()->toArray(); $beforeLedger=WaterLedger::query()->count();
- $db->beginTransaction();
- try {
- $baseline=$service->dashboard([]);
- $rows=[
- 'all'=>['audience'=>'all','sort'=>60],
- 'new'=>['audience'=>'new','region'=>'上海','sort'=>50,'starts_at'=>'2026-09-17 12:00:00'],
- 'purchased'=>['audience'=>'purchased','region'=>'上海','sort'=>50],
- 'admin'=>['audience'=>'enterprise_admin','region'=>'上海','sort'=>40],
- 'scheduled'=>['audience'=>'all','starts_at'=>'2026-09-17 12:00:01','sort'=>30],
- 'expired'=>['audience'=>'all','ends_at'=>'2026-09-17 11:59:59','sort'=>20],
- 'disabled'=>['audience'=>'all','status'=>2,'sort'=>70],
- 'end'=>['audience'=>'all','ends_at'=>'2026-09-17 12:00:00','sort'=>55],
- 'guangzhou'=>['audience'=>'new','region'=>'广州','sort'=>45],
- ];
- $ids=[];
- foreach ($rows as $key=>$input) $ids[$key]=$business->save('content',['title'=>$tag.'-'.$key]+$input,1)['id'];
- $first=$service->dashboard(['keyword'=>' '.$tag.' ','limit'=>4]);
- $second=$service->dashboard(['keyword'=>$tag,'limit'=>4,'page'=>2]);
- $third=$service->dashboard(['keyword'=>$tag,'limit'=>4,'page'=>3]);
- check($first['list']['total']===9 && count($first['list']['data'])===4 && count($third['list']['data'])===1,'4/4/1 pages');
- $expected=[$ids['disabled'],$ids['all'],$ids['end'],$ids['purchased'],$ids['new'],$ids['guangzhou'],$ids['admin'],$ids['scheduled'],$ids['expired']];
- $combined=array_merge($first['list']['data'],$second['list']['data'],$third['list']['data']);
- check(array_column($combined,'id')===$expected,'sort descending then ID descending');
- check($first['summary']===$second['summary'],'summary independent of pagination');
- check($first['summary']['total']-$baseline['summary']['total']===9,'global total');
- foreach (['active'=>6,'scheduled'=>1,'expired'=>1,'disabled'=>1] as $status=>$count) {
- check($first['summary'][$status]-$baseline['summary'][$status]===$count,'global status '.$status);
- $filtered=$service->dashboard(['keyword'=>$tag,'status'=>$status]);
- check($filtered['list']['total']===$count && count(array_filter($filtered['list']['data'],fn($r)=>$r['delivery_status']===$status))===$count,'status filter '.$status);
- check($filtered['summary']===$first['summary'],'global count independent of '.$status.' filter');
- }
- check($service->dashboard(['keyword'=>$tag.'-missing'])['summary']===$first['summary'],'global count independent of empty search');
- check($service->dashboard(['keyword'=>$tag,'page'=>99])['list']['data']===[],'out of range page');
- check($service->dashboard(['keyword'=>$tag.'-new'])['list']['total']===1,'title search');
- check($service->dashboard(['keyword'=>'上海'])['list']['total']>=3,'region search');
- $fixtureIds=function(array $preview) use ($ids): array { return array_values(array_intersect(array_column($preview['banners'],'id'),array_values($ids))); };
- check($fixtureIds($service->preview([]))===[$ids['all'],$ids['end']],'unknown region only nationwide');
- check($fixtureIds($service->preview(['region'=>' 上海 ']))===[$ids['all'],$ids['end'],$ids['new']],'new customer Shanghai and exact start');
- check($fixtureIds($service->preview(['region'=>'上海','purchase_status'=>'purchased']))===[$ids['all'],$ids['end'],$ids['purchased']],'purchased excludes new and admin');
- check($fixtureIds($service->preview(['region'=>'上海','purchase_status'=>'purchased','is_enterprise_admin'=>1]))===[$ids['all'],$ids['end'],$ids['purchased'],$ids['admin']],'admin plus purchased audiences');
- check($fixtureIds($service->preview(['region'=>'上海','is_enterprise_admin'=>'1']))===[$ids['all'],$ids['end'],$ids['new'],$ids['admin']],'admin plus new audiences');
- check($fixtureIds($service->preview(['region'=>'广州']))===[$ids['all'],$ids['end'],$ids['guangzhou']],'separate region');
- check($fixtureIds($service->preview(['region'=>'上海市']))===[$ids['all'],$ids['end']],'region names use exact matching');
- check($fixtureIds($service->preview(['region'=>'全国']))===[$ids['all'],$ids['end']],'national label normalized');
- $after=new WaterContentDashboard($now->modify('+1 second'));
- check(!in_array($ids['end'],$fixtureIds($after->preview([])),true) && in_array($ids['scheduled'],$fixtureIds($after->preview([])),true),'end expiration and start activation by time');
- $utc=new WaterContentDashboard(new DateTimeImmutable('2026-09-17 04:00:00',new DateTimeZone('UTC')));
- check($utc->preview(['region'=>'上海'])===$service->preview(['region'=>'上海']),'Shanghai timezone normalization');
- $edit=WaterContent::query()->find($ids['new'])->toArray();
- $business->save('content',array_replace($edit,['status'=>2,'title'=>$tag.'-edited','region'=>'全国']),1);
- $persisted=WaterContent::query()->find($ids['new']);
- check($persisted->title===$tag.'-edited' && $persisted->region==='' && (int)$persisted->status===2,'edit persisted and national region normalized');
- check(!in_array($ids['new'],$fixtureIds($service->preview(['region'=>'上海'])),true),'disable immediately removes banner');
- $business->save('content',$edit,1);
- check(in_array($ids['new'],$fixtureIds($service->preview(['region'=>'上海'])),true),'re-enable restores delivery');
- check(WaterLedger::query()->where('reference','content:'.$ids['new'])->count()===3,'create and both edits audited');
- $sku=$business->save('sku',['name'=>$tag.'-sku','liters'=>18.9],1)['id'];
- $offSku=$business->save('sku',['name'=>$tag.'-off','liters'=>18.9,'status'=>2],1)['id'];
- $package=$business->save('package',['name'=>$tag.'-package','sku_id'=>$sku,'buckets'=>30,'price_cents'=>69900],1)['id'];
- $disabledPackage=$business->save('package',['name'=>$tag.'-disabled','sku_id'=>$sku,'buckets'=>10,'price_cents'=>19900,'status'=>2],1)['id'];
- $disabledSkuPackage=$business->save('package',['name'=>$tag.'-disabled-sku','sku_id'=>$offSku,'buckets'=>10,'price_cents'=>29900],1)['id'];
- $packages=$service->preview([])['packages']; $packageIds=array_column($packages,'id');
- check(in_array($package,$packageIds,true),'preview uses persisted enabled package');
- check(!in_array($disabledPackage,$packageIds,true) && !in_array($disabledSkuPackage,$packageIds,true),'disabled SKU and package excluded');
- $packageRow=array_column($packages,null,'id')[$package];
- check($packageRow['price_cents']===69900 && $packageRow['buckets']===30,'price and entitlement match actual package');
- $invalids=[['title'=>' '],['title'=>[]],['title'=>str_repeat('字',121)],['audience'=>'agent'],['region'=>[]],['image_url'=>'javascript:alert(1)'],['target_url'=>'data:text/html,x'],['target_url'=>'https://'],['image_url'=>[]],['starts_at'=>'2026-02-30 00:00:00'],['ends_at'=>'yesterday'],['starts_at'=>[]],['starts_at'=>"2026-09-17\0"],['starts_at'=>'2026-09-18 00:00:00','ends_at'=>'2026-09-17 00:00:00'],['starts_at'=>'2026-09-17 00:00:00','ends_at'=>'2026-09-17 00:00:00'],['sort'=>-1],['sort'=>'1.5'],['status'=>'1.5']];
- $contentCount=WaterContent::query()->count(); $auditCount=WaterLedger::query()->count();
- foreach ($invalids as $invalid) rejects(fn()=>$business->save('content',array_replace(['title'=>$tag,'audience'=>'all'],$invalid),1),'invalid content rejected');
- check(WaterContent::query()->count()===$contentCount && WaterLedger::query()->count()===$auditCount,'invalid input creates no content or audit');
- foreach ([['keyword'=>[]],['status'=>'unknown'],['page'=>0],['limit'=>101]] as $invalid) rejects(fn()=>$service->dashboard($invalid),'invalid dashboard filters');
- foreach ([['region'=>[]],['purchase_status'=>'agent'],['is_enterprise_admin'=>2],['is_enterprise_admin'=>[]]] as $invalid) rejects(fn()=>$service->preview($invalid),'invalid preview context');
- $reflection=new ReflectionClass(WaterController::class); $controller=$reflection->newInstanceWithoutConstructor();
- $reflection->getProperty('adminId')->setValue($controller,0);
- rejects(fn()=>$controller->contentDashboard(request('GET','/core/water/content/dashboard')),'unauthorized dashboard');
- rejects(fn()=>$controller->contentPreview(request('GET','/core/water/content/preview')),'unauthorized audience preview');
- rejects(fn()=>$controller->save(request('POST','/core/water/content',['title'=>$tag,'audience'=>'all']),'content'),'unauthorized save');
- $reflection->getProperty('adminId')->setValue($controller,1);
- $response=json_decode($controller->contentDashboard(request('GET','/core/water/content/dashboard',['keyword'=>$tag]))->rawBody(),true);
- check(($response['code']??0)===200 && $response['data']['list']['total']===9,'dashboard controller');
- check((json_decode($controller->contentPreview(request('GET','/core/water/content/preview'))->rawBody(),true)['code']??0)===200,'preview controller');
- $response=json_decode($controller->save(request('POST','/core/water/content',['title'=>$tag.'-json','audience'=>'all','image_url'=>'https://example.invalid/banner.png','target_url'=>'https://example.invalid/activity']),'content')->rawBody(),true);
- check(($response['code']??0)===200 && WaterContent::query()->find($response['data']['id'])->target_url==='https://example.invalid/activity','JSON save path and safe URL roundtrip');
- } finally { $db->rollBack(); }
- check(WaterContent::query()->orderBy('id')->get()->toArray()===$before && WaterLedger::query()->count()===$beforeLedger,'original content and audits restored after rollback');
- check(!WaterSku::query()->where('name','like',$tag.'%')->exists() && !WaterPackage::query()->where('name','like',$tag.'%')->exists(),'package fixtures rolled back');
- if (in_array('--http',$argv,true)) {
- $token=\Tinywan\Jwt\JwtToken::generateToken(['id'=>1,'username'=>'admin','plat'=>'saiadmin','type'=>'web','client'=>$tag,'access_exp'=>120,'refresh_exp'=>120])['access_token'];
- foreach (['dashboard?limit=1','preview?purchase_status=new®ion='.urlencode('上海'),'preview?purchase_status=purchased&is_enterprise_admin=1'] as $path) {
- $ch=curl_init('http://127.0.0.1:8787/core/water/content/'.$path);
- curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token]]);
- $raw=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); $body=json_decode((string)$raw,true);
- check($status===200 && ($body['code']??0)===200,'HTTP '.$path);
- if (isset($body['data']['banners'])) foreach ($body['data']['banners'] as $row) check($row['delivery_status']==='active','HTTP only active banners');
- }
- $result=(new WaterContentDashboard())->dashboard([]);
- echo 'Current content: total='.$result['summary']['total'].', active='.$result['summary']['active']."\n";
- }
- echo "PASS: $checks content checks; all fixtures rolled back.\n";
|