LCOV - code coverage report
Current view: top level - frontend/handlers - storage_read_create_session_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 188 188
Test Date: 2026-07-02 21:01:18 Functions: 100.0 % 17 17

            Line data    Source code
       1              : // Direct (no gRPC socket) tests for `StorageReadService::CreateReadSession`.
       2              : 
       3              : #include "frontend/handlers/storage_read_internal.h"
       4              : #include "frontend/handlers/storage_read_test_fixture.h"
       5              : #include "grpcpp/grpcpp.h"
       6              : 
       7              : namespace bigquery_emulator {
       8              : namespace frontend {
       9              : namespace {
      10            1 : TEST_F(StorageReadServiceTest, CreateReadSessionReturnsSessionStreamAndSchema) {
      11            1 :   CreatePeopleTable();
      12            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
      13            1 :   v1::ReadSession resp;
      14            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
      15            2 :   ASSERT_TRUE(status.ok()) << status.error_message();
      16              : 
      17              :   // Session name uses the canonical
      18              :   // `projects/{p}/locations/-/sessions/s{N}` shape so the gateway can
      19              :   // synthesize matching BigQuery REST IDs.
      20            1 :   EXPECT_EQ(resp.name(), "projects/proj-test/locations/-/sessions/s1");
      21              : 
      22              :   // Table is echoed verbatim so the caller does not need to re-derive it.
      23            1 :   EXPECT_EQ(resp.table(), "projects/proj-test/datasets/ds/tables/t");
      24              : 
      25              :   // Schema is attached so a follow-up ReadRows does not have to
      26              :   // round-trip back through DescribeTable. The handler sends every
      27              :   // column in declaration order.
      28            1 :   ASSERT_EQ(resp.schema().fields_size(), 3);
      29            1 :   EXPECT_EQ(resp.schema().fields(0).name(), "id");
      30            1 :   EXPECT_EQ(resp.schema().fields(0).type(), "INT64");
      31            1 :   EXPECT_EQ(resp.schema().fields(0).mode(), "REQUIRED");
      32            1 :   EXPECT_EQ(resp.schema().fields(1).name(), "name");
      33            1 :   EXPECT_EQ(resp.schema().fields(1).type(), "STRING");
      34            1 :   EXPECT_EQ(resp.schema().fields(2).name(), "tags");
      35            1 :   EXPECT_EQ(resp.schema().fields(2).type(), "STRING");
      36            1 :   EXPECT_EQ(resp.schema().fields(2).mode(), "REPEATED");
      37              : 
      38              :   // Exactly one stream today; `max_stream_count`-driven parallel
      39              :   // streams are not implemented. The stream id nests under the
      40              :   // session name.
      41            1 :   ASSERT_EQ(resp.streams_size(), 1);
      42            1 :   EXPECT_EQ(resp.streams(0).name(),
      43            1 :             "projects/proj-test/locations/-/sessions/s1/streams/0");
      44              : 
      45              :   // SessionsForTesting reflects the mint we just did (and only that
      46              :   // one). This is the test hook the ReadRows tests use to assert
      47              :   // the lookup actually consults the session map.
      48            1 :   EXPECT_EQ(service_->SessionsForTesting(), 1u);
      49            1 : }
      50              : 
      51            1 : TEST_F(StorageReadServiceTest, CreateReadSessionEchoesReadOptions) {
      52            1 :   CreatePeopleTable();
      53            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
      54            1 :   auto* options = req.mutable_read_session()->mutable_read_options();
      55            1 :   options->add_selected_fields("id");
      56            1 :   options->add_selected_fields("name");
      57              :   // The handler parses the row_restriction at session-mint
      58              :   // time. `id = 0` is the canonical happy-path shape (single
      59              :   // `<column> = <literal>` equality); range / connective forms now
      60              :   // surface INVALID_ARGUMENT from CreateReadSession, which has its
      61              :   // own dedicated test below.
      62            1 :   options->set_row_restriction("id = 0");
      63            1 :   v1::ReadSession resp;
      64            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
      65            2 :   ASSERT_TRUE(status.ok()) << status.error_message();
      66              : 
      67              :   // The proto contract says read_options round-trips on the reply so
      68              :   // the gateway can show the caller what shape the session was minted
      69              :   // with. The handler honors selected_fields, so the
      70              :   // response *schema* reflects the projection (id, name only)
      71              :   // while the read_options round-trip still echoes the request.
      72            1 :   ASSERT_EQ(resp.read_options().selected_fields_size(), 2);
      73            1 :   EXPECT_EQ(resp.read_options().selected_fields(0), "id");
      74            1 :   EXPECT_EQ(resp.read_options().selected_fields(1), "name");
      75            1 :   EXPECT_EQ(resp.read_options().row_restriction(), "id = 0");
      76              : 
      77              :   // The response schema is projected to the caller's
      78              :   // selected_fields list, in caller-supplied order. The full table
      79              :   // has three columns; pinning "id" + "name" must drop "tags".
      80            1 :   ASSERT_EQ(resp.schema().fields_size(), 2);
      81            1 :   EXPECT_EQ(resp.schema().fields(0).name(), "id");
      82            1 :   EXPECT_EQ(resp.schema().fields(1).name(), "name");
      83            1 : }
      84              : 
      85            1 : TEST_F(StorageReadServiceTest, CreateReadSessionRejectsUnknownSelectedField) {
      86            1 :   CreatePeopleTable();
      87            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
      88              :   // `phone` is not a column on the people table; the handler rejects
      89              :   // this at session-mint time so the streaming RPC never starts
      90              :   // against an invalid projection.
      91            1 :   req.mutable_read_session()->mutable_read_options()->add_selected_fields(
      92            1 :       "phone");
      93            1 :   v1::ReadSession resp;
      94            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
      95            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
      96            2 :       << status.error_message();
      97            1 :   EXPECT_EQ(service_->SessionsForTesting(), 0u);
      98            1 : }
      99              : 
     100            1 : TEST_F(StorageReadServiceTest, CreateReadSessionRejectsEmptySelectedField) {
     101            1 :   CreatePeopleTable();
     102            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     103              :   // An empty entry is a wire-level oddity but the handler refuses
     104              :   // it explicitly so a buggy client cannot smuggle a no-match
     105              :   // projection that would silently approximate "all columns".
     106            1 :   req.mutable_read_session()->mutable_read_options()->add_selected_fields("");
     107            1 :   v1::ReadSession resp;
     108            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     109            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     110            2 :       << status.error_message();
     111            1 : }
     112              : 
     113            1 : TEST_F(StorageReadServiceTest, CreateReadSessionAcceptsRangeRestriction) {
     114            1 :   CreatePeopleTable();
     115            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     116            1 :   req.mutable_read_session()->mutable_read_options()->set_row_restriction(
     117            1 :       "id > 0");
     118            1 :   v1::ReadSession resp;
     119            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     120            2 :   ASSERT_TRUE(status.ok()) << status.error_message();
     121            1 :   EXPECT_EQ(resp.read_options().row_restriction(), "id > 0");
     122            1 : }
     123              : 
     124              : TEST_F(StorageReadServiceTest,
     125            1 :        CreateReadSessionRejectsUnknownColumnRestriction) {
     126            1 :   CreatePeopleTable();
     127            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     128            1 :   req.mutable_read_session()->mutable_read_options()->set_row_restriction(
     129            1 :       "missing = 1");
     130            1 :   v1::ReadSession resp;
     131            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     132            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     133            2 :       << status.error_message();
     134            1 : }
     135              : 
     136            1 : TEST_F(StorageReadServiceTest, CreateReadSessionMintsUniqueSessionIds) {
     137            1 :   CreatePeopleTable();
     138            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     139              : 
     140            1 :   v1::ReadSession first;
     141            1 :   v1::ReadSession second;
     142            1 :   ASSERT_TRUE(service_->CreateReadSession(nullptr, &req, &first).ok());
     143            1 :   ASSERT_TRUE(service_->CreateReadSession(nullptr, &req, &second).ok());
     144            1 :   EXPECT_NE(first.name(), second.name());
     145            1 :   EXPECT_EQ(service_->SessionsForTesting(), 2u);
     146            1 : }
     147              : 
     148              : // ---------------------------------------------------------------------------
     149              : // CreateReadSession: validation errors
     150              : // ---------------------------------------------------------------------------
     151              : 
     152            1 : TEST_F(StorageReadServiceTest, CreateReadSessionEmptyParentIsInvalidArgument) {
     153            1 :   CreatePeopleTable();
     154            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     155            1 :   req.set_parent("");
     156            1 :   v1::ReadSession resp;
     157            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     158            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     159            2 :       << status.error_message();
     160            1 :   EXPECT_EQ(service_->SessionsForTesting(), 0u);
     161            1 : }
     162              : 
     163              : TEST_F(StorageReadServiceTest,
     164            1 :        CreateReadSessionMalformedParentIsInvalidArgument) {
     165            1 :   CreatePeopleTable();
     166            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     167              :   // Missing the `projects/` prefix; matches the public BigQuery
     168              :   // surface, which rejects the same shape.
     169            1 :   req.set_parent("proj-test");
     170            1 :   v1::ReadSession resp;
     171            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     172            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     173            2 :       << status.error_message();
     174            1 : }
     175              : 
     176            1 : TEST_F(StorageReadServiceTest, CreateReadSessionEmptyTableIsInvalidArgument) {
     177            1 :   CreatePeopleTable();
     178            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     179            1 :   req.mutable_read_session()->set_table("");
     180            1 :   v1::ReadSession resp;
     181            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     182            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     183            2 :       << status.error_message();
     184            1 : }
     185              : 
     186              : TEST_F(StorageReadServiceTest,
     187            1 :        CreateReadSessionMalformedTableIsInvalidArgument) {
     188            1 :   CreatePeopleTable();
     189            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     190              :   // Drop the `tables/` segment so the path no longer has 6 parts.
     191            1 :   req.mutable_read_session()->set_table("projects/proj-test/datasets/ds/t");
     192            1 :   v1::ReadSession resp;
     193            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     194            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     195            2 :       << status.error_message();
     196            1 : }
     197              : 
     198              : TEST_F(StorageReadServiceTest,
     199            1 :        CreateReadSessionParentTableProjectMismatchIsInvalidArgument) {
     200            1 :   CreatePeopleTable();
     201            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     202            1 :   req.set_parent("projects/other-proj");
     203            1 :   v1::ReadSession resp;
     204            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     205            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     206            2 :       << status.error_message();
     207            1 :   EXPECT_EQ(service_->SessionsForTesting(), 0u);
     208            1 : }
     209              : 
     210              : TEST_F(StorageReadServiceTest,
     211            1 :        CreateReadSessionAllowsPublicDataTableWithCallerParent) {
     212            1 :   backend::schema::TableSchema schema;
     213            1 :   backend::schema::ColumnSchema name;
     214            1 :   name.name = "name";
     215            1 :   name.type = backend::schema::ColumnType::kString;
     216            1 :   schema.columns.push_back(name);
     217            1 :   ASSERT_TRUE(
     218            1 :       storage_->CreateDataset({internal::kPublicDataProject, "usa_names"}, "US")
     219            1 :           .ok());
     220            1 :   ASSERT_TRUE(
     221            1 :       storage_
     222            1 :           ->CreateTable({internal::kPublicDataProject, "usa_names", "names"},
     223            1 :                         schema)
     224            1 :           .ok());
     225              : 
     226            1 :   v1::CreateReadSessionRequest req;
     227            1 :   req.set_parent("projects/dev");
     228            1 :   req.mutable_read_session()->set_table(
     229            1 :       "projects/bigquery-public-data/datasets/usa_names/tables/names");
     230            1 :   v1::ReadSession resp;
     231            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     232            2 :   ASSERT_TRUE(status.ok()) << status.error_message();
     233            1 :   EXPECT_EQ(service_->SessionsForTesting(), 1u);
     234            1 : }
     235              : 
     236            1 : TEST_F(StorageReadServiceTest, CreateReadSessionMissingTableIsNotFound) {
     237              :   // Dataset + project exist but the table does not. Storage::GetSchema
     238              :   // returns NotFound and the handler maps that onto gRPC NOT_FOUND.
     239            1 :   ASSERT_TRUE(storage_->CreateDataset({"proj-test", "ds"}, "US").ok());
     240            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     241            1 :   v1::ReadSession resp;
     242            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     243            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::NOT_FOUND)
     244            2 :       << status.error_message();
     245            1 : }
     246              : 
     247            1 : TEST_F(StorageReadServiceTest, CreateReadSessionMissingDatasetIsNotFound) {
     248              :   // Neither dataset nor table exist; same NOT_FOUND mapping but
     249              :   // through a different storage path.
     250            1 :   v1::CreateReadSessionRequest req = MakePeopleRequest();
     251            1 :   v1::ReadSession resp;
     252            1 :   ::grpc::Status status = service_->CreateReadSession(nullptr, &req, &resp);
     253            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::NOT_FOUND)
     254            2 :       << status.error_message();
     255            1 : }
     256              : 
     257              : // ---------------------------------------------------------------------------
     258              : // ReadRows: validation-only tests that do not require a real
     259              : // ServerWriter. Happy-path tests live in the StorageReadGrpcTest
     260              : // fixture below; they need an in-process server because
     261              : // ::grpc::ServerWriter is concrete and not designed to be mocked.
     262              : // ---------------------------------------------------------------------------
     263              : 
     264            1 : TEST_F(StorageReadServiceTest, ReadRowsRejectsMalformedReadStream) {
     265            1 :   v1::ReadRowsRequest req;
     266              :   // Missing the `/streams/0` suffix entirely. The handler only mints
     267              :   // streams/0 and refuses every other shape.
     268            1 :   req.set_read_stream("projects/proj-test/locations/-/sessions/s1");
     269            1 :   ::grpc::Status status = service_->ReadRows(nullptr, &req, nullptr);
     270            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     271            2 :       << status.error_message();
     272            1 : }
     273              : 
     274            1 : TEST_F(StorageReadServiceTest, ReadRowsRejectsEmptyReadStream) {
     275            1 :   v1::ReadRowsRequest req;
     276            1 :   ::grpc::Status status = service_->ReadRows(nullptr, &req, nullptr);
     277            2 :   EXPECT_EQ(status.error_code(), ::grpc::StatusCode::INVALID_ARGUMENT)
     278            2 :       << status.error_message();
     279            1 : }
     280              : }  // namespace
     281              : }  // namespace frontend
     282              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1