11import { describe , expect , it , vi } from "vitest" ;
2- import type { NetworkRule , NetworkRuleState } from "@App/app/repo/network_rule" ;
2+ import type { NetworkRule , NetworkRuleCondition , NetworkRuleState } from "@App/app/repo/network_rule" ;
33import { compileNetworkRules , DeclarativeNetRequestUserRuleApplier } from "./network_rule_compiler" ;
44import {
55 INSTALL_GUARD_RULE_ID_MAX ,
@@ -22,12 +22,17 @@ type DnrMock = typeof chrome.declarativeNetRequest & {
2222
2323const dnr = chrome . declarativeNetRequest as DnrMock ;
2424
25- function rule ( id : string , action : NetworkRule [ "action" ] , enabled = true ) : NetworkRule {
25+ function rule (
26+ id : string ,
27+ action : NetworkRule [ "action" ] ,
28+ enabled = true ,
29+ condition : NetworkRuleCondition = { requestDomains : [ `${ id } .example.com` ] }
30+ ) : NetworkRule {
2631 return {
2732 id,
2833 name : id ,
2934 enabled,
30- condition : { requestDomains : [ ` ${ id } .example.com` ] } ,
35+ condition,
3136 action,
3237 createdAt : 1 ,
3338 updatedAt : 1 ,
@@ -81,6 +86,249 @@ function requestPhase(rules: chrome.declarativeNetRequest.Rule[]): chrome.declar
8186}
8287
8388describe ( "网络规则 DNR 编译与对账" , ( ) => {
89+ it ( "相邻的纯响应头移除规则合并 requestDomains,并保留最高优先级" , ( ) => {
90+ const compiled = compileNetworkRules (
91+ state ( [
92+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
93+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
94+ ] )
95+ ) ;
96+
97+ expect ( compiled ) . toHaveLength ( 1 ) ;
98+ expect ( compiled [ 0 ] ) . toMatchObject ( {
99+ id : USER_RULE_ID_MIN ,
100+ priority : 2 ,
101+ action : {
102+ type : "modifyHeaders" ,
103+ responseHeaders : [ { header : "content-security-policy" , operation : "remove" } ] ,
104+ } ,
105+ condition : {
106+ requestDomains : [ "a.example.com" , "b.example.com" ] ,
107+ } ,
108+ } ) ;
109+ } ) ;
110+
111+ it ( "100 条单域名规则合并为一条,超过 100 个域名时按物理上限分块" , ( ) => {
112+ const rules = Array . from ( { length : 101 } , ( _ , index ) =>
113+ rule ( `site-${ String ( index ) . padStart ( 3 , "0" ) } ` , {
114+ type : "removeResponseHeaders" ,
115+ headers : [ "content-security-policy" ] ,
116+ } )
117+ ) ;
118+
119+ const compiled = compileNetworkRules ( state ( rules ) ) ;
120+
121+ expect ( compiled ) . toHaveLength ( 2 ) ;
122+ expect ( compiled [ 0 ] . condition . requestDomains ) . toHaveLength ( 100 ) ;
123+ expect ( compiled [ 1 ] . condition . requestDomains ) . toEqual ( [ "site-100.example.com" ] ) ;
124+ expect ( compiled . flatMap ( ( item ) => item . condition . requestDomains ) ) . toEqual (
125+ rules . map ( ( item ) => item . condition . requestDomains ! [ 0 ] ) . sort ( )
126+ ) ;
127+ expect ( compiled . map ( ( item ) => item . priority ) ) . toEqual ( [ 101 , 101 ] ) ;
128+ expect ( compiled . map ( ( item ) => item . id ) ) . toEqual ( [ USER_RULE_ID_MIN , USER_RULE_ID_MIN + 1 ] ) ;
129+ } ) ;
130+
131+ it ( "合并前去重并消除已被父域覆盖的 requestDomains" , ( ) => {
132+ const compiled = compileNetworkRules (
133+ state ( [
134+ rule ( "parent" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
135+ requestDomains : [ "example.com" , "foo.example.com" , "badexample.com" ] ,
136+ } ) ,
137+ rule ( "child" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
138+ requestDomains : [ "example.com" , "bar.foo.example.com" , "other.com" ] ,
139+ } ) ,
140+ ] )
141+ ) ;
142+
143+ expect ( compiled ) . toHaveLength ( 1 ) ;
144+ expect ( compiled [ 0 ] . condition . requestDomains ) . toEqual ( [ "badexample.com" , "example.com" , "other.com" ] ) ;
145+ } ) ;
146+
147+ it ( "不把 IPv4-like 或 IPv6-like host 当作可做父域消除的 DNS domain" , ( ) => {
148+ const compiled = compileNetworkRules (
149+ state ( [
150+ rule ( "ipv4-parent" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
151+ requestDomains : [ "2.3.4" ] ,
152+ } ) ,
153+ rule ( "ipv4-child" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
154+ requestDomains : [ "1.2.3.4" ] ,
155+ } ) ,
156+ rule ( "ipv6-parent" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
157+ requestDomains : [ "[2001:db8::1]" ] ,
158+ } ) ,
159+ rule ( "ipv6-child" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
160+ requestDomains : [ "[2001:db8::2]" ] ,
161+ } ) ,
162+ ] )
163+ ) ;
164+
165+ expect ( compiled . map ( ( item ) => item . condition . requestDomains ) ) . toEqual ( [
166+ [ "1.2.3.4" , "2.3.0.4" , "[2001:db8::1]" , "[2001:db8::2]" ] ,
167+ ] ) ;
168+ } ) ;
169+
170+ it ( "compiled condition 的 set-like array 顺序不同仍可合并" , ( ) => {
171+ const compiled = compileNetworkRules (
172+ state ( [
173+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
174+ requestDomains : [ "a.example.com" ] ,
175+ resourceTypes : [ "script" , "main_frame" ] ,
176+ requestMethods : [ "post" , "get" ] ,
177+ } ) ,
178+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
179+ requestDomains : [ "b.example.com" ] ,
180+ resourceTypes : [ "main_frame" , "script" ] ,
181+ requestMethods : [ "get" , "post" ] ,
182+ } ) ,
183+ ] )
184+ ) ;
185+
186+ expect ( compiled ) . toHaveLength ( 1 ) ;
187+ expect ( compiled [ 0 ] . condition . requestDomains ) . toEqual ( [ "a.example.com" , "b.example.com" ] ) ;
188+ } ) ;
189+
190+ it . each ( [
191+ [
192+ "resourceTypes" ,
193+ { requestDomains : [ "a.example.com" ] , resourceTypes : [ "main_frame" ] } ,
194+ { requestDomains : [ "b.example.com" ] , resourceTypes : [ "script" ] } ,
195+ ] ,
196+ [
197+ "requestMethods" ,
198+ { requestDomains : [ "a.example.com" ] , requestMethods : [ "get" ] } ,
199+ { requestDomains : [ "b.example.com" ] , requestMethods : [ "post" ] } ,
200+ ] ,
201+ [
202+ "excludedRequestDomains" ,
203+ { requestDomains : [ "a.example.com" ] , excludedRequestDomains : [ "cdn.example.com" ] } ,
204+ { requestDomains : [ "b.example.com" ] , excludedRequestDomains : [ "static.example.com" ] } ,
205+ ] ,
206+ [
207+ "urlFilter" ,
208+ { requestDomains : [ "a.example.com" ] , urlFilter : "*/a/*" } ,
209+ { requestDomains : [ "b.example.com" ] , urlFilter : "*/b/*" } ,
210+ ] ,
211+ ] as Array < [ string , NetworkRuleCondition , NetworkRuleCondition ] > ) (
212+ "除 requestDomains 外的 %s 差异是 barrier" ,
213+ ( _field , firstCondition , secondCondition ) => {
214+ const compiled = compileNetworkRules (
215+ state ( [
216+ rule (
217+ "barrier-a" ,
218+ { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ,
219+ true ,
220+ firstCondition
221+ ) ,
222+ rule (
223+ "barrier-b" ,
224+ { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ,
225+ true ,
226+ secondCondition
227+ ) ,
228+ ] )
229+ ) ;
230+
231+ expect ( compiled ) . toHaveLength ( 2 ) ;
232+ }
233+ ) ;
234+
235+ it ( "不同的响应头移除 action 不合并" , ( ) => {
236+ const compiled = compileNetworkRules (
237+ state ( [
238+ rule ( "csp" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
239+ rule ( "csp-frame" , {
240+ type : "removeResponseHeaders" ,
241+ headers : [ "content-security-policy" , "x-frame-options" ] ,
242+ } ) ,
243+ ] )
244+ ) ;
245+
246+ expect ( compiled ) . toHaveLength ( 2 ) ;
247+ } ) ;
248+
249+ it ( "相同的响应头移除集合即使排列不同也可合并,并输出 canonical action" , ( ) => {
250+ const compiled = compileNetworkRules (
251+ state ( [
252+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "x-test" , "content-security-policy" ] } ) ,
253+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" , "x-test" ] } ) ,
254+ ] )
255+ ) ;
256+
257+ expect ( compiled ) . toHaveLength ( 1 ) ;
258+ expect ( compiled [ 0 ] . action . responseHeaders ) . toEqual ( [
259+ { header : "content-security-policy" , operation : "remove" } ,
260+ { header : "x-test" , operation : "remove" } ,
261+ ] ) ;
262+ } ) ;
263+
264+ it ( "active rule barrier 不能被跨越,但 disabled rule 不阻断合并" , ( ) => {
265+ const baseRules = [
266+ rule ( "first" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
267+ rule ( "barrier" , { type : "allow" } , false ) ,
268+ rule ( "last" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
269+ ] ;
270+
271+ expect ( compileNetworkRules ( state ( baseRules ) ) ) . toHaveLength ( 1 ) ;
272+
273+ const enabledRules = baseRules . map ( ( item ) => ( item . id === "barrier" ? { ...item , enabled : true } : item ) ) ;
274+ expect ( compileNetworkRules ( state ( enabledRules ) ) ) . toHaveLength ( 3 ) ;
275+ } ) ;
276+
277+ it ( "没有 requestDomains 的规则不参与合并" , ( ) => {
278+ const compiled = compileNetworkRules (
279+ state ( [
280+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
281+ urlFilter : "*/a/*" ,
282+ } ) ,
283+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
284+ urlFilter : "*/b/*" ,
285+ } ) ,
286+ ] )
287+ ) ;
288+
289+ expect ( compiled ) . toHaveLength ( 2 ) ;
290+ } ) ;
291+
292+ it ( "空 requestDomains 也不参与合并" , ( ) => {
293+ const compiled = compileNetworkRules (
294+ state ( [
295+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
296+ requestDomains : [ ] ,
297+ } ) ,
298+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } , true , {
299+ requestDomains : [ ] ,
300+ } ) ,
301+ ] )
302+ ) ;
303+
304+ expect ( compiled ) . toHaveLength ( 2 ) ;
305+ } ) ;
306+
307+ it ( "非纯响应头移除 action 不参与合并" , ( ) => {
308+ const compiled = compileNetworkRules (
309+ state ( [
310+ rule ( "a" , { type : "modifyResponseHeaders" , headers : [ { header : "x-test" , operation : "remove" } ] } ) ,
311+ rule ( "b" , { type : "modifyResponseHeaders" , headers : [ { header : "x-test" , operation : "remove" } ] } ) ,
312+ ] )
313+ ) ;
314+
315+ expect ( compiled ) . toHaveLength ( 2 ) ;
316+ } ) ;
317+
318+ it ( "编译 deterministic 且不 mutate source state" , ( ) => {
319+ const input = state ( [
320+ rule ( "a" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
321+ rule ( "b" , { type : "removeResponseHeaders" , headers : [ "content-security-policy" ] } ) ,
322+ ] ) ;
323+ const original = structuredClone ( input ) ;
324+
325+ const first = compileNetworkRules ( input ) ;
326+ const second = compileNetworkRules ( input ) ;
327+
328+ expect ( first ) . toEqual ( second ) ;
329+ expect ( input ) . toEqual ( original ) ;
330+ } ) ;
331+
84332 it ( "一次应用把两条不同动作的规则编译成保留段内的两条 DNR 规则,回收段内陈旧 ID 并保留段外规则" , async ( ) => {
85333 dnr . resetMock ( ) ;
86334 await chrome . declarativeNetRequest . updateDynamicRules ( {
0 commit comments