|
8 | 8 | //! shared `resolve_tenant_ref` helper, mirroring the already-shipped |
9 | 9 | //! `CREATE TENANT <name>` / `SHOW TENANT <name>` paths. |
10 | 10 |
|
11 | | -use crate::common::pgwire_auth_helpers::{ddl_err, ddl_ok, make_state_with_catalog, superuser}; |
| 11 | +use crate::common::pgwire_auth_helpers::{ |
| 12 | + ddl_err, ddl_ok, make_state, make_state_with_catalog, superuser, |
| 13 | +}; |
12 | 14 |
|
13 | 15 | // ─── DROP TENANT by name ───────────────────────────────────────────────────── |
14 | 16 |
|
@@ -89,12 +91,7 @@ async fn alter_tenant_by_numeric_id_still_works() { |
89 | 91 | let su = superuser(); |
90 | 92 |
|
91 | 93 | ddl_ok(&state, &su, "CREATE TENANT acme_alter_num ID 7145").await; |
92 | | - ddl_ok( |
93 | | - &state, |
94 | | - &su, |
95 | | - "ALTER TENANT 7145 SET QUOTA max_qps = 250", |
96 | | - ) |
97 | | - .await; |
| 94 | + ddl_ok(&state, &su, "ALTER TENANT 7145 SET QUOTA max_qps = 250").await; |
98 | 95 | } |
99 | 96 |
|
100 | 97 | /// `ALTER TENANT <name> SET QUOTA ...` — name resolves to id. |
@@ -129,3 +126,205 @@ async fn alter_tenant_unknown_name_errors() { |
129 | 126 | "expected 42704/does not exist, got: {err}" |
130 | 127 | ); |
131 | 128 | } |
| 129 | + |
| 130 | +/// `ALTER TENANT '<name>' SET QUOTA ...` — single-quoted name resolves to id, |
| 131 | +/// matching the quoted-name path already covered on DROP. |
| 132 | +#[tokio::test] |
| 133 | +async fn alter_tenant_by_quoted_name() { |
| 134 | + let state = make_state_with_catalog(); |
| 135 | + let su = superuser(); |
| 136 | + |
| 137 | + ddl_ok(&state, &su, "CREATE TENANT acme_alter_quoted ID 7147").await; |
| 138 | + ddl_ok( |
| 139 | + &state, |
| 140 | + &su, |
| 141 | + "ALTER TENANT 'acme_alter_quoted' SET QUOTA max_qps = 250", |
| 142 | + ) |
| 143 | + .await; |
| 144 | +} |
| 145 | + |
| 146 | +/// `ALTER TENANT '' SET QUOTA ...` (empty quoted name) → `42601`, the same |
| 147 | +/// resolver guard exercised on DROP. |
| 148 | +#[tokio::test] |
| 149 | +async fn alter_tenant_empty_name_errors() { |
| 150 | + let state = make_state_with_catalog(); |
| 151 | + let su = superuser(); |
| 152 | + |
| 153 | + let err = ddl_err(&state, &su, "ALTER TENANT '' SET QUOTA max_qps = 250").await; |
| 154 | + assert!( |
| 155 | + err.contains("42601") && err.contains("numeric id or a tenant name"), |
| 156 | + "expected 42601 empty-name error, got: {err}" |
| 157 | + ); |
| 158 | +} |
| 159 | + |
| 160 | +// ─── PURGE TENANT by name ──────────────────────────────────────────────────── |
| 161 | +// |
| 162 | +// `purge_tenant` resolves the tenant reference, rejects the system tenant, and |
| 163 | +// only then dispatches the destructive meta op to the Data Plane. The |
| 164 | +// resolution and guard branches all return before that dispatch, so they are |
| 165 | +// covered here without a Data Plane; the post-dispatch happy path is exercised |
| 166 | +// by the executor-level purge tests. |
| 167 | + |
| 168 | +/// `PURGE TENANT <name> CONFIRM` on an unknown name → `42704`, before any |
| 169 | +/// destructive dispatch. |
| 170 | +#[tokio::test] |
| 171 | +async fn purge_tenant_unknown_name_errors() { |
| 172 | + let state = make_state_with_catalog(); |
| 173 | + let su = superuser(); |
| 174 | + |
| 175 | + let err = ddl_err(&state, &su, "PURGE TENANT no_such_tenant CONFIRM").await; |
| 176 | + assert!( |
| 177 | + err.contains("does not exist") && err.contains("42704"), |
| 178 | + "expected 42704/does not exist, got: {err}" |
| 179 | + ); |
| 180 | +} |
| 181 | + |
| 182 | +/// `PURGE TENANT '' CONFIRM` (empty quoted name) → `42601` resolver guard. |
| 183 | +#[tokio::test] |
| 184 | +async fn purge_tenant_empty_name_errors() { |
| 185 | + let state = make_state_with_catalog(); |
| 186 | + let su = superuser(); |
| 187 | + |
| 188 | + let err = ddl_err(&state, &su, "PURGE TENANT '' CONFIRM").await; |
| 189 | + assert!( |
| 190 | + err.contains("42601") && err.contains("numeric id or a tenant name"), |
| 191 | + "expected 42601 empty-name error, got: {err}" |
| 192 | + ); |
| 193 | +} |
| 194 | + |
| 195 | +/// `PURGE TENANT <name>` with a malformed confirmation token resolves the name |
| 196 | +/// first, then fails the `CONFIRM` gate with `42601`. The `42601`/CONFIRM error |
| 197 | +/// (rather than `42704`) proves the name resolved — covering the by-name purge |
| 198 | +/// path up to the Data Plane boundary. |
| 199 | +#[tokio::test] |
| 200 | +async fn purge_tenant_by_name_resolves_then_requires_confirm() { |
| 201 | + let state = make_state_with_catalog(); |
| 202 | + let su = superuser(); |
| 203 | + |
| 204 | + ddl_ok(&state, &su, "CREATE TENANT acme_purge_name ID 7148").await; |
| 205 | + let err = ddl_err(&state, &su, "PURGE TENANT acme_purge_name PLEASE").await; |
| 206 | + assert!( |
| 207 | + err.contains("42601") && err.contains("CONFIRM"), |
| 208 | + "expected name to resolve then hit the CONFIRM gate, got: {err}" |
| 209 | + ); |
| 210 | +} |
| 211 | + |
| 212 | +/// `PURGE TENANT <id>` (numeric) for an existing tenant with a malformed |
| 213 | +/// confirmation token — the legacy numeric path resolves and passes the |
| 214 | +/// existence gate, then fails the `CONFIRM` gate. Regression that the resolver |
| 215 | +/// refactor kept the numeric fast path intact for purge. |
| 216 | +#[tokio::test] |
| 217 | +async fn purge_tenant_by_numeric_id_resolves_then_requires_confirm() { |
| 218 | + let state = make_state_with_catalog(); |
| 219 | + let su = superuser(); |
| 220 | + |
| 221 | + ddl_ok(&state, &su, "CREATE TENANT acme_purge_num ID 7149").await; |
| 222 | + let err = ddl_err(&state, &su, "PURGE TENANT 7149 PLEASE").await; |
| 223 | + assert!( |
| 224 | + err.contains("42601") && err.contains("CONFIRM"), |
| 225 | + "expected numeric id to resolve then hit the CONFIRM gate, got: {err}" |
| 226 | + ); |
| 227 | +} |
| 228 | + |
| 229 | +// ─── Unknown numeric id parity (id form must match name form) ───────────────── |
| 230 | +// |
| 231 | +// A numeric id that matches no tenant must behave exactly like an unknown |
| 232 | +// name: `42704`, or an `IF EXISTS` no-op for DROP. Before the existence gate |
| 233 | +// these silently proceeded (DROP proposed a delete, ALTER seeded a default |
| 234 | +// quota, PURGE dispatched a destructive op) — the id/name asymmetry. |
| 235 | + |
| 236 | +/// `DROP TENANT <unknown_id>` without `IF EXISTS` → `42704`, matching the |
| 237 | +/// unknown-name behavior. |
| 238 | +#[tokio::test] |
| 239 | +async fn drop_tenant_unknown_numeric_id_without_if_exists_errors() { |
| 240 | + let state = make_state_with_catalog(); |
| 241 | + let su = superuser(); |
| 242 | + |
| 243 | + let err = ddl_err(&state, &su, "DROP TENANT 999001").await; |
| 244 | + assert!( |
| 245 | + err.contains("42704") && err.contains("does not exist"), |
| 246 | + "expected 42704 for unknown numeric id, got: {err}" |
| 247 | + ); |
| 248 | +} |
| 249 | + |
| 250 | +/// `DROP TENANT IF EXISTS <unknown_id>` is a no-op success, matching the |
| 251 | +/// unknown-name `IF EXISTS` behavior. |
| 252 | +#[tokio::test] |
| 253 | +async fn drop_tenant_if_exists_unknown_numeric_id_is_noop() { |
| 254 | + let state = make_state_with_catalog(); |
| 255 | + let su = superuser(); |
| 256 | + |
| 257 | + ddl_ok(&state, &su, "DROP TENANT IF EXISTS 999002").await; |
| 258 | +} |
| 259 | + |
| 260 | +/// `ALTER TENANT <unknown_id> SET QUOTA ...` → `42704`, never a silent |
| 261 | +/// default-quota seed for a phantom id. |
| 262 | +#[tokio::test] |
| 263 | +async fn alter_tenant_unknown_numeric_id_errors() { |
| 264 | + let state = make_state_with_catalog(); |
| 265 | + let su = superuser(); |
| 266 | + |
| 267 | + let err = ddl_err(&state, &su, "ALTER TENANT 999003 SET QUOTA max_qps = 250").await; |
| 268 | + assert!( |
| 269 | + err.contains("42704") && err.contains("does not exist"), |
| 270 | + "expected 42704 for unknown numeric id, got: {err}" |
| 271 | + ); |
| 272 | +} |
| 273 | + |
| 274 | +/// `PURGE TENANT <unknown_id> CONFIRM` → `42704`, never a destructive dispatch |
| 275 | +/// for a tenant that does not exist. |
| 276 | +#[tokio::test] |
| 277 | +async fn purge_tenant_unknown_numeric_id_errors() { |
| 278 | + let state = make_state_with_catalog(); |
| 279 | + let su = superuser(); |
| 280 | + |
| 281 | + let err = ddl_err(&state, &su, "PURGE TENANT 999004 CONFIRM").await; |
| 282 | + assert!( |
| 283 | + err.contains("42704") && err.contains("does not exist"), |
| 284 | + "expected 42704 for unknown numeric id, got: {err}" |
| 285 | + ); |
| 286 | +} |
| 287 | + |
| 288 | +// ─── System tenant + catalog-unavailable guards ────────────────────────────── |
| 289 | + |
| 290 | +/// `DROP TENANT 0` — the system tenant is protected with `42501` regardless of |
| 291 | +/// the resolver refactor. |
| 292 | +#[tokio::test] |
| 293 | +async fn drop_system_tenant_numeric_errors() { |
| 294 | + let state = make_state_with_catalog(); |
| 295 | + let su = superuser(); |
| 296 | + |
| 297 | + let err = ddl_err(&state, &su, "DROP TENANT 0").await; |
| 298 | + assert!( |
| 299 | + err.contains("42501") && err.contains("system tenant"), |
| 300 | + "expected 42501 system-tenant guard, got: {err}" |
| 301 | + ); |
| 302 | +} |
| 303 | + |
| 304 | +/// `PURGE TENANT 0 CONFIRM` — the system tenant cannot be purged. |
| 305 | +#[tokio::test] |
| 306 | +async fn purge_system_tenant_numeric_errors() { |
| 307 | + let state = make_state_with_catalog(); |
| 308 | + let su = superuser(); |
| 309 | + |
| 310 | + let err = ddl_err(&state, &su, "PURGE TENANT 0 CONFIRM").await; |
| 311 | + assert!( |
| 312 | + err.contains("42501") && err.contains("system tenant"), |
| 313 | + "expected 42501 system-tenant guard, got: {err}" |
| 314 | + ); |
| 315 | +} |
| 316 | + |
| 317 | +/// `DROP TENANT <name>` with no catalog wired up → `42601`, directing the |
| 318 | +/// caller to use a numeric id. Exercises the catalog-unavailable branch of |
| 319 | +/// `resolve_tenant_ref` that the catalog-backed fixtures cannot reach. |
| 320 | +#[tokio::test] |
| 321 | +async fn drop_tenant_by_name_without_catalog_errors() { |
| 322 | + let state = make_state(); |
| 323 | + let su = superuser(); |
| 324 | + |
| 325 | + let err = ddl_err(&state, &su, "DROP TENANT some_tenant_name").await; |
| 326 | + assert!( |
| 327 | + err.contains("42601") && err.contains("catalog"), |
| 328 | + "expected 42601 catalog-unavailable error, got: {err}" |
| 329 | + ); |
| 330 | +} |
0 commit comments