|
@@ -1159,3 +1159,58 @@ TEST_F(ApiFixture, RelationsEnforcedOnParentPermitsDelete) {
|
|
|
ASSERT_TRUE(off); EXPECT_EQ(off->status, 200);
|
|
ASSERT_TRUE(off); EXPECT_EQ(off->status, 200);
|
|
|
EXPECT_EQ(c.Delete((base + "/cust2/documents/" + pid).c_str())->status, 200);
|
|
EXPECT_EQ(c.Delete((base + "/cust2/documents/" + pid).c_str())->status, 200);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+TEST_F(ApiFixture, AdminReadonlyLockBlocksWritesThenReleases) {
|
|
|
|
|
+ auto c = admin();
|
|
|
|
|
+ std::string base = "/api/v1/projects/" + project_ + "/collections";
|
|
|
|
|
+ ASSERT_EQ(c.Post(base.c_str(), nlohmann::json{{"name","rolock"},{"kind","json"}}.dump(),
|
|
|
|
|
+ "application/json")->status, 201);
|
|
|
|
|
+ std::string docs = base + "/rolock/documents";
|
|
|
|
|
+
|
|
|
|
|
+ // RAII guard: unconditionally releases the server-global lock when the test
|
|
|
|
|
+ // function returns (including on an early ASSERT_* failure), so a mid-test
|
|
|
|
|
+ // assertion can never leave the DB locked for every later test in the suite.
|
|
|
|
|
+ struct UnlockGuard {
|
|
|
|
|
+ httplib::Client& c;
|
|
|
|
|
+ ~UnlockGuard() {
|
|
|
|
|
+ c.Put("/api/v1/admin/readonly", nlohmann::json{{"readonly", false}}.dump(),
|
|
|
|
|
+ "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+ } unlockGuard{c};
|
|
|
|
|
+
|
|
|
|
|
+ auto on = c.Put("/api/v1/admin/readonly", nlohmann::json{{"readonly", true}}.dump(),
|
|
|
|
|
+ "application/json");
|
|
|
|
|
+ ASSERT_TRUE(on); EXPECT_EQ(on->status, 200);
|
|
|
|
|
+ // GET is now authoritative (backed by the DB's own getReadOnlyStatus()), not a
|
|
|
|
|
+ // locally-tracked flag -- assert it reflects what the server itself reports.
|
|
|
|
|
+ auto st1 = c.Get("/api/v1/admin/readonly");
|
|
|
|
|
+ ASSERT_TRUE(st1); EXPECT_EQ(st1->status, 200);
|
|
|
|
|
+ auto st1Json = nlohmann::json::parse(st1->body);
|
|
|
|
|
+ EXPECT_TRUE(st1Json["readonly"].get<bool>());
|
|
|
|
|
+ EXPECT_EQ(st1Json["scope"], "server");
|
|
|
|
|
+
|
|
|
|
|
+ auto blocked = c.Post(docs.c_str(), nlohmann::json{{"v",1}}.dump(), "application/json");
|
|
|
|
|
+ ASSERT_TRUE(blocked); EXPECT_NE(blocked->status, 201);
|
|
|
|
|
+
|
|
|
|
|
+ auto off = c.Put("/api/v1/admin/readonly", nlohmann::json{{"readonly", false}}.dump(),
|
|
|
|
|
+ "application/json");
|
|
|
|
|
+ ASSERT_TRUE(off); EXPECT_EQ(off->status, 200);
|
|
|
|
|
+ auto st2 = c.Get("/api/v1/admin/readonly");
|
|
|
|
|
+ ASSERT_TRUE(st2); EXPECT_EQ(st2->status, 200);
|
|
|
|
|
+ EXPECT_FALSE(nlohmann::json::parse(st2->body)["readonly"].get<bool>());
|
|
|
|
|
+ EXPECT_EQ(c.Post(docs.c_str(), nlohmann::json{{"v",2}}.dump(), "application/json")->status, 201);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+TEST_F(ApiFixture, ReadonlyRequiresAdmin) {
|
|
|
|
|
+ // The fixture's scoped-key helper is used by the existing scoped-key tests;
|
|
|
|
|
+ // reuse the same construction to assert a non-admin key is refused.
|
|
|
|
|
+ auto c = admin();
|
|
|
|
|
+ auto made = c.Post("/api/v1/keys", nlohmann::json{
|
|
|
|
|
+ {"label","ro-nonadmin"}, {"projects", nlohmann::json::array({project_})},
|
|
|
|
|
+ {"admin", false}}.dump(), "application/json");
|
|
|
|
|
+ ASSERT_EQ(made->status, 201);
|
|
|
|
|
+ std::string nonAdmin = nlohmann::json::parse(made->body)["key"];
|
|
|
|
|
+ auto r = request("PUT", "/api/v1/admin/readonly",
|
|
|
|
|
+ nlohmann::json{{"readonly", true}}.dump(), "application/json", nonAdmin);
|
|
|
|
|
+ ASSERT_TRUE(r); EXPECT_EQ(r->status, 403);
|
|
|
|
|
+}
|