|
| 1 | +// Copyright 2026 apstndb |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package mycli_test |
| 16 | + |
| 17 | +// Dispatch-level READONLY guard regression for the extracted BIGQUERY family |
| 18 | +// (#775, re-homed by #778). These live in the external mycli_test package so |
| 19 | +// they can import feature/bigquery (which imports mycli) without a cycle, and |
| 20 | +// prove the guard fires through the real dispatch path: build the statement from |
| 21 | +// the merged def table exactly as production does, then execute it against a |
| 22 | +// READONLY session. |
| 23 | + |
| 24 | +import ( |
| 25 | + "testing" |
| 26 | + |
| 27 | + "github.com/apstndb/spanner-mycli/internal/mycli" |
| 28 | + "github.com/apstndb/spanner-mycli/internal/mycli/feature/bigquery" |
| 29 | +) |
| 30 | + |
| 31 | +func buildBigQuery(t *testing.T, sql string) mycli.Statement { |
| 32 | + t.Helper() |
| 33 | + defs := mycli.MergedStatementDefs(bigquery.Feature()) |
| 34 | + stmt, err := mycli.BuildStatementWithDefs(defs, "BIGQUERY "+sql) |
| 35 | + if err != nil { |
| 36 | + t.Fatalf("BuildStatementWithDefs(BIGQUERY %s) error = %v", sql, err) |
| 37 | + } |
| 38 | + return stmt |
| 39 | +} |
| 40 | + |
| 41 | +// TestReadOnlyGuardBlocksMutatingBigQuery verifies that mutating BIGQUERY SQL is |
| 42 | +// rejected by Session.ExecuteStatement in READONLY mode before the statement can |
| 43 | +// reach BigQuery (no client is built). |
| 44 | +func TestReadOnlyGuardBlocksMutatingBigQuery(t *testing.T) { |
| 45 | + t.Parallel() |
| 46 | + |
| 47 | + for _, tt := range []struct { |
| 48 | + desc string |
| 49 | + sql string |
| 50 | + }{ |
| 51 | + {desc: "DELETE blocked", sql: "DELETE FROM dataset.table WHERE TRUE"}, |
| 52 | + {desc: "CREATE blocked", sql: "CREATE TABLE dataset.table AS SELECT 1"}, |
| 53 | + {desc: "unrecognized keyword blocked", sql: "FROBNICATE dataset.table"}, |
| 54 | + } { |
| 55 | + t.Run(tt.desc, func(t *testing.T) { |
| 56 | + t.Parallel() |
| 57 | + session := mycli.NewReadOnlySessionForTest(t) |
| 58 | + _, err := session.ExecuteStatement(t.Context(), buildBigQuery(t, tt.sql)) |
| 59 | + if !mycli.IsReadOnlyError(err) { |
| 60 | + t.Errorf("%s in READONLY mode: got error %v, want READONLY error", tt.desc, err) |
| 61 | + } |
| 62 | + }) |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +// TestReadOnlyGuardAllowsReadOnlyBigQuery verifies that read-only BIGQUERY SQL |
| 67 | +// passes the READONLY guard: dispatch reaches Execute, which then fails for an |
| 68 | +// unrelated reason (no BigQuery project configured), NOT with the READONLY |
| 69 | +// error. This proves, through the real guard, that a SELECT/WITH is not wrongly |
| 70 | +// blocked. A statically-mutating classification is also ruled out. |
| 71 | +func TestReadOnlyGuardAllowsReadOnlyBigQuery(t *testing.T) { |
| 72 | + t.Parallel() |
| 73 | + |
| 74 | + for _, tt := range []struct { |
| 75 | + desc string |
| 76 | + sql string |
| 77 | + }{ |
| 78 | + {desc: "SELECT", sql: "SELECT 1"}, |
| 79 | + {desc: "WITH", sql: "WITH cte AS (SELECT 1) SELECT * FROM cte"}, |
| 80 | + } { |
| 81 | + t.Run(tt.desc, func(t *testing.T) { |
| 82 | + t.Parallel() |
| 83 | + stmt := buildBigQuery(t, tt.sql) |
| 84 | + if _, isMutation := stmt.(mycli.MutationStatement); isMutation { |
| 85 | + t.Fatal("BigQueryStatement must not be a static MutationStatement") |
| 86 | + } |
| 87 | + |
| 88 | + session := mycli.NewReadOnlySessionForTest(t) |
| 89 | + _, err := session.ExecuteStatement(t.Context(), stmt) |
| 90 | + // The guard let it through; Execute then fails on missing project |
| 91 | + // config, which must NOT be the READONLY sentinel. |
| 92 | + if err == nil { |
| 93 | + t.Fatalf("%s: expected a non-READONLY error from Execute (no project configured), got nil", tt.desc) |
| 94 | + } |
| 95 | + if mycli.IsReadOnlyError(err) { |
| 96 | + t.Errorf("%s BIGQUERY wrongly blocked by READONLY guard: %v", tt.desc, err) |
| 97 | + } |
| 98 | + }) |
| 99 | + } |
| 100 | +} |
0 commit comments