|
1 | | -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core'; |
2 | 1 | import { describe, expect, it } from 'vitest'; |
3 | 2 | import { targetsCloudflareInternalTable } from '../../src/utils/internalSqlQuery'; |
4 | 3 |
|
5 | | -// Runs the same sanitize -> summarize -> filter pipeline as `instrumentSqlStorage`, so the tests |
6 | | -// exercise the real detection path rather than hand-written summaries. |
7 | | -const check = (query: string, allowlist?: Array<string | RegExp>): boolean => { |
8 | | - const sanitized = _INTERNAL_sanitizeSqlQuery(query); |
9 | | - return targetsCloudflareInternalTable(_INTERNAL_getSqlQuerySummary(sanitized), allowlist, sanitized); |
10 | | -}; |
11 | | - |
| 4 | +// Behavioural coverage of the filter lives in `instrumentSqlStorage.test.ts`, which drives real |
| 5 | +// queries through the instrumented `exec`. What remains here are the signature-level contracts that |
| 6 | +// call path cannot reach: an absent summary, and an absent `queryText`. |
12 | 7 | describe('targetsCloudflareInternalTable', () => { |
13 | | - describe('internal queries (cf_ tables)', () => { |
14 | | - it.each([ |
15 | | - ['SELECT', 'SELECT * FROM cf_agents_state WHERE id = ?'], |
16 | | - ['INSERT', 'INSERT INTO cf_agents_fibers (id, callback) VALUES (?, ?)'], |
17 | | - ['DELETE', 'DELETE FROM cf_agents_schedules WHERE id = ?'], |
18 | | - ['UPDATE', 'UPDATE cf_agent_tool_runs SET output_json = ? WHERE id = ?'], |
19 | | - ['CREATE TABLE', 'CREATE TABLE IF NOT EXISTS cf_agents_workflows (id TEXT PRIMARY KEY NOT NULL)'], |
20 | | - ['ALTER TABLE', 'ALTER TABLE cf_agents_queues ADD COLUMN retry_options TEXT'], |
21 | | - ['DROP TABLE', 'DROP TABLE cf_agents_state'], |
22 | | - ['cf_agent_ prefix', 'SELECT * FROM cf_agent_identity'], |
23 | | - ['cf_ai_ prefix', 'INSERT INTO cf_ai_chat_stream_chunks (id) VALUES (?)'], |
24 | | - ['cf_mcp_ prefix', 'SELECT * FROM cf_mcp_agent_event'], |
25 | | - ['schema version', 'SELECT version FROM cf_schema_version'], |
26 | | - // SQLite upsert forms used by the agents framework for state/schedule/MCP persistence |
27 | | - ['INSERT OR REPLACE', 'INSERT OR REPLACE INTO cf_agents_state (id, state) VALUES (?, ?)'], |
28 | | - [ |
29 | | - 'INSERT OR REPLACE with column list', |
30 | | - `INSERT OR REPLACE INTO cf_agents_mcp_servers ( id, name, server_url, client_id, auth_url, |
31 | | - callback_url, server_options ) |
32 | | - VALUES ( ?, ?, ?, ?, ?, ?, ? )`, |
33 | | - ], |
34 | | - ['INSERT OR IGNORE', 'INSERT OR IGNORE INTO cf_agents_sub_agents (class, name) VALUES (?, ?)'], |
35 | | - ['REPLACE INTO', 'REPLACE INTO cf_agents_queues (id, payload) VALUES (?, ?)'], |
36 | | - ['UPDATE OR REPLACE', 'UPDATE OR REPLACE cf_agents_state SET state = ? WHERE id = ?'], |
37 | | - ])('returns true for %s on internal tables', (_label, query) => { |
38 | | - expect(check(query)).toBe(true); |
39 | | - }); |
40 | | - |
41 | | - // The summary of a CREATE INDEX carries the index name, not the indexed table — the cf_ |
42 | | - // target only exists in the ON clause of the full statement. |
43 | | - it.each([ |
44 | | - [ |
45 | | - 'framework statement', |
46 | | - `create index if not exists idx_ai_chat_agent_tool_request_id |
47 | | - on cf_ai_chat_agent_tool_runs(request_id)`, |
48 | | - ], |
49 | | - ['uppercase', 'CREATE INDEX idx_agents_state_id ON cf_agents_state (id)'], |
50 | | - ['UNIQUE', 'CREATE UNIQUE INDEX idx_agents_state_id ON cf_agents_state (id)'], |
51 | | - ['without IF NOT EXISTS', 'CREATE INDEX idx_chunks_stream ON cf_ai_chat_stream_chunks (stream_id)'], |
52 | | - ])('returns true for CREATE INDEX (%s) on an internal table', (_label, query) => { |
53 | | - expect(check(query)).toBe(true); |
54 | | - }); |
55 | | - |
56 | | - it('returns true for an internal JOIN', () => { |
57 | | - const query = ` |
58 | | - SELECT f.fiber_id, f.status |
59 | | - FROM cf_agents_fibers f |
60 | | - LEFT JOIN cf_agents_runs r ON r.id = f.fiber_id |
61 | | - WHERE f.status IN ('pending', 'running') |
62 | | - `; |
63 | | - expect(check(query)).toBe(true); |
64 | | - }); |
65 | | - |
66 | | - it('returns true when an internal table is joined with a user table', () => { |
67 | | - // `.some()` — any internal table present means the query is framework-driven noise. |
68 | | - expect(check('SELECT * FROM cf_agents_state s JOIN users u ON u.id = s.id')).toBe(true); |
69 | | - }); |
70 | | - |
71 | | - it('handles case-insensitive keywords and prefixes', () => { |
72 | | - expect(check('select * from CF_AGENTS_STATE')).toBe(true); |
73 | | - }); |
| 8 | + it.each([ |
| 9 | + ['undefined', undefined], |
| 10 | + ['empty', ''], |
| 11 | + ])('returns false for a %s summary', (_label, summary) => { |
| 12 | + expect(targetsCloudflareInternalTable(summary)).toBe(false); |
74 | 13 | }); |
75 | 14 |
|
76 | | - describe('user queries (must be instrumented)', () => { |
77 | | - it.each([ |
78 | | - ['SELECT', 'SELECT * FROM users WHERE id = ?'], |
79 | | - ['INSERT', 'INSERT INTO orders (id, total) VALUES (?, ?)'], |
80 | | - ['UPDATE', 'UPDATE products SET price = ? WHERE id = ?'], |
81 | | - ['DELETE', 'DELETE FROM sessions WHERE expired = 1'], |
82 | | - ['CREATE TABLE', 'CREATE TABLE users (id TEXT PRIMARY KEY)'], |
83 | | - ['CREATE INDEX', 'CREATE INDEX idx_name ON users (name)'], |
84 | | - ['table with cf in the middle', 'SELECT * FROM my_cf_table'], |
85 | | - ['table starting with cfg', 'SELECT * FROM cfg_settings'], |
86 | | - ['INSERT OR REPLACE', 'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?)'], |
87 | | - ['REPLACE INTO', 'REPLACE INTO sessions (id, token) VALUES (?, ?)'], |
88 | | - ['UPDATE OR IGNORE', 'UPDATE OR IGNORE products SET price = ? WHERE id = ?'], |
89 | | - ])('returns false for %s on user tables', (_label, query) => { |
90 | | - expect(check(query)).toBe(false); |
91 | | - }); |
| 15 | + it('falls back to the summary when no queryText is passed', () => { |
| 16 | + expect(targetsCloudflareInternalTable('SELECT cf_agents_state')).toBe(true); |
| 17 | + expect(targetsCloudflareInternalTable('SELECT users')).toBe(false); |
92 | 18 | }); |
93 | 19 |
|
94 | | - describe('allowlist (opt a cf_ table back into instrumentation)', () => { |
95 | | - it('returns false for an allowlisted table matched by exact string', () => { |
96 | | - expect(check('SELECT * FROM cf_my_table', ['cf_my_table'])).toBe(false); |
97 | | - }); |
98 | | - |
99 | | - it('returns false for an allowlisted table matched by regex', () => { |
100 | | - expect(check('SELECT * FROM cf_reports_daily', [/^cf_reports_/])).toBe(false); |
101 | | - }); |
102 | | - |
103 | | - it('returns false for an allowlisted table targeted by an upsert', () => { |
104 | | - expect(check('INSERT OR REPLACE INTO cf_my_table (id) VALUES (?)', ['cf_my_table'])).toBe(false); |
105 | | - }); |
106 | | - |
107 | | - it('returns false for CREATE INDEX on an allowlisted table', () => { |
108 | | - expect(check('CREATE INDEX idx_mine ON cf_my_table (id)', ['cf_my_table'])).toBe(false); |
109 | | - }); |
110 | | - |
111 | | - it('requires an exact match for string entries', () => { |
112 | | - // Substring matches must not opt a table back in, otherwise `cf_` would allowlist everything. |
113 | | - expect(check('SELECT * FROM cf_agents_state', ['cf_agents'])).toBe(true); |
114 | | - }); |
115 | | - |
116 | | - it('still skips genuine internal tables that are not allowlisted', () => { |
117 | | - expect(check('SELECT * FROM cf_agents_state', ['cf_my_table'])).toBe(true); |
118 | | - }); |
119 | | - |
120 | | - it('still skips when an internal table is joined with an allowlisted table', () => { |
121 | | - expect(check('SELECT * FROM cf_my_table t JOIN cf_agents_state s ON s.id = t.id', ['cf_my_table'])).toBe(true); |
122 | | - }); |
123 | | - |
124 | | - it('ignores an empty allowlist', () => { |
125 | | - expect(check('SELECT * FROM cf_agents_state', [])).toBe(true); |
126 | | - }); |
127 | | - }); |
128 | | - |
129 | | - describe('summaries without a resolvable table target (safe default: instrument)', () => { |
130 | | - it.each([ |
131 | | - ['no-table SELECT', 'SELECT 1'], |
132 | | - ['PRAGMA', 'PRAGMA foreign_keys = ON'], |
133 | | - ['bare operation', 'BEGIN'], |
134 | | - ])('returns false for %s', (_label, query) => { |
135 | | - expect(check(query)).toBe(false); |
136 | | - }); |
137 | | - |
138 | | - it.each([ |
139 | | - ['undefined', undefined], |
140 | | - ['empty', ''], |
141 | | - ])('returns false for a %s summary', (_label, summary) => { |
142 | | - expect(targetsCloudflareInternalTable(summary)).toBe(false); |
143 | | - }); |
| 20 | + // Without queryText a CREATE INDEX summary carries the index name, so the cf_ table in the ON |
| 21 | + // clause is invisible and the query is instrumented — the caller must pass queryText to filter it. |
| 22 | + it('cannot resolve a CREATE INDEX target from the summary alone', () => { |
| 23 | + expect(targetsCloudflareInternalTable('CREATE INDEX idx_agents_state_id')).toBe(false); |
144 | 24 | }); |
145 | 25 | }); |
0 commit comments