From eedd1c6b6a612daeb0e4e154bc0200df6826aa1d Mon Sep 17 00:00:00 2001 From: MetroWind Date: Fri, 12 Sep 2025 14:41:42 -0700 Subject: Implement items() and addLink() in data source. --- src/app.hpp | 4 + src/app_test.cpp | 227 +++++++++++------------------------------------------- src/data.cpp | 88 +++++++++++++++++++-- src/data.hpp | 6 +- src/data_test.cpp | 26 ++++++- 5 files changed, 160 insertions(+), 191 deletions(-) (limited to 'src') diff --git a/src/app.hpp b/src/app.hpp index 092196c..09c7143 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -27,6 +27,10 @@ public: std::string urlFor(const std::string& name, const std::string& arg="") const; + void handleIndex(Response& res) const; + void handleLogin(Response& res) const; + void handleOpenIDRedirect(const Request& req, Response& res) const; + private: void setup() override; diff --git a/src/app_test.cpp b/src/app_test.cpp index a022611..06f2084 100644 --- a/src/app_test.cpp +++ b/src/app_test.cpp @@ -19,187 +19,46 @@ using ::testing::HasSubstr; using ::testing::FieldsAre; using ::testing::ContainsRegex; -void PrintTo(const ShortLink& link, std::ostream* os) -{ - *os << "ShortLink(id: " << link.id - << ", shortcut: " << link.shortcut - << ", original_url: " << link.original_url - << ", type: " << link.type - << ", user_id: " << link.user_id - << ", user_name: " << link.user_name - << ", visits: " << link.visits - << ", time_creation: " << link.time_creation << ")"; -} - -class UserAppTest : public testing::Test -{ -protected: - UserAppTest() - { - config.base_url = "http://localhost:8080/"; - config.listen_address = "localhost"; - config.listen_port = 8080; - config.data_dir = "."; - - auto auth = std::make_unique(); - - mw::UserInfo expected_user; - expected_user.name = "mw"; - expected_user.id = "mw"; - mw::Tokens token; - token.access_token = "aaa"; - EXPECT_CALL(*auth, getUser(std::move(token))) - .Times(::testing::AtLeast(0)) - .WillRepeatedly(Return(expected_user)); - auto data = std::make_unique(); - data_source = data.get(); - - app = std::make_unique(config, std::move(data), std::move(auth)); - } - - Configuration config; - std::unique_ptr app; - const DataSourceMock* data_source; -}; - -TEST_F(UserAppTest, CanDenyAccessToLinkList) -{ - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( - mw::HTTPRequest("http://localhost:8080/_/links"))); - EXPECT_EQ(res->status, 401); - } - app->stop(); - app->wait(); -} - -TEST_F(UserAppTest, CanHandleLinkList) -{ - std::vector links; - ShortLink link0; - link0.shortcut = "link0"; - link0.original_url = "a"; - link0.id = 1; - link0.user_id = "mw"; - link0.user_name = "mw"; - link0.type = ShortLink::NORMAL; - ShortLink link1; - link1.shortcut = "link1"; - link1.original_url = "b"; - link1.id = 2; - link1.user_id = "mw"; - link1.user_name = "mw"; - link1.type = ShortLink::REGEXP; - links.push_back(std::move(link0)); - links.push_back(std::move(link1)); - - EXPECT_CALL(*data_source, getAllLinks("mw")) - .WillOnce(Return(std::move(links))); - - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( - mw::HTTPRequest("http://localhost:8080/_/links") - .addHeader("Cookie", "shrt-access-token=aaa"))); - EXPECT_EQ(res->status, 200) << "Response body: " << res->payloadAsStr(); - EXPECT_THAT(res->payloadAsStr(), ContainsRegex("a[[:space:]]*-")); - EXPECT_THAT(res->payloadAsStr(), ContainsRegex("b[[:space:]]*✅")); - } - app->stop(); - app->wait(); -} - -TEST_F(UserAppTest, CanDenyAccessToNewLink) -{ - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( - mw::HTTPRequest("http://localhost:8080/_/new-link"))); - EXPECT_EQ(res->status, 401); - } - app->stop(); - app->wait(); -} - -TEST_F(UserAppTest, CanHandleNewLink) -{ - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( - mw::HTTPRequest("http://localhost:8080/_/new-link") - .addHeader("Cookie", "shrt-access-token=aaa"))); - EXPECT_EQ(res->status, 200); - EXPECT_THAT(res->payloadAsStr(), HasSubstr("Create a New Link")); - } - app->stop(); - app->wait(); -} - -TEST_F(UserAppTest, CanDenyAccessToCreateLink) -{ - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.post( - mw::HTTPRequest("http://localhost:8080/_/create-link"))); - EXPECT_EQ(res->status, 401); - } - app->stop(); - app->wait(); -} - -TEST_F(UserAppTest, CanDenyHandleCreateLink) -{ - EXPECT_CALL(*data_source, addLink( - FieldsAre( - _, // id - "abc", // shortcut - "http://darksair.org", // original_url - ShortLink::NORMAL, // type - "mw", // user_id - "", // user_name - _, // visits - _))) // time_creation - .WillOnce(Return(mw::E())); - - EXPECT_CALL(*data_source, addLink( - FieldsAre( - _, // id - "xyz", // shortcut - "http://mws.rocks", // original_url - ShortLink::REGEXP, // type - "mw", // user_id - "", // user_name - _, // visits - _))) // time_creation - .WillOnce(Return(mw::E())); - - EXPECT_TRUE(mw::isExpected(app->start())); - { - mw::HTTPSession client; - ASSIGN_OR_FAIL(const mw::HTTPResponse* res1, client.post( - mw::HTTPRequest("http://localhost:8080/_/create-link") - .setPayload("shortcut=abc&original_url=http%3A%2F%2Fdarksair%2Eorg" - "®exp=off") - .addHeader("Cookie", "shrt-access-token=aaa") - .setContentType("application/x-www-form-urlencoded"))); - EXPECT_EQ(res1->status, 302); - EXPECT_EQ(res1->header.at("Location"), "http://localhost:8080/"); - - ASSIGN_OR_FAIL(const mw::HTTPResponse* res2, client.post( - mw::HTTPRequest("http://localhost:8080/_/create-link") - .setPayload("shortcut=xyz&original_url=http%3A%2F%2Fmws%2Erocks" - "®exp=on") - .addHeader("Cookie", "shrt-access-token=aaa") - .setContentType("application/x-www-form-urlencoded"))); - EXPECT_EQ(res2->status, 302); - EXPECT_EQ(res2->header.at("Location"), "http://localhost:8080/"); - } - app->stop(); - app->wait(); -} +// class UserAppTest : public testing::Test +// { +// protected: +// UserAppTest() +// { +// config.base_url = "http://localhost:8080/"; +// config.listen_address = "localhost"; +// config.listen_port = 8080; +// config.data_dir = "."; + +// auto auth = std::make_unique(); + +// mw::UserInfo expected_user; +// expected_user.name = "mw"; +// expected_user.id = "mw"; +// mw::Tokens token; +// token.access_token = "aaa"; +// EXPECT_CALL(*auth, getUser(std::move(token))) +// .Times(::testing::AtLeast(0)) +// .WillRepeatedly(Return(expected_user)); +// auto data = std::make_unique(); +// data_source = data.get(); + +// app = std::make_unique(config, std::move(data), std::move(auth)); +// } + +// Configuration config; +// std::unique_ptr app; +// const DataSourceMock* data_source; +// }; + +// TEST_F(UserAppTest, CanDenyAccessToLinkList) +// { +// EXPECT_TRUE(mw::isExpected(app->start())); +// { +// mw::HTTPSession client; +// ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( +// mw::HTTPRequest("http://localhost:8080/_/links"))); +// EXPECT_EQ(res->status, 401); +// } +// app->stop(); +// app->wait(); +// } diff --git a/src/data.cpp b/src/data.cpp index 78cb500..d6c4c5f 100644 --- a/src/data.cpp +++ b/src/data.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,8 +9,33 @@ #include "data.hpp" +// id, owner_id, parent_id, name, url, desc, visibility, time. +#define LINK_TUPLE_TYPES int64_t,int64_t,int64_t,std::string,std::string,std::string,int,int64_t + namespace { +using LinkTuple = std::tuple; +LinkItem linkFromTuple(const LinkTuple& t) +{ + LinkItem l; + l.id = std::get<0>(t); + l.owner_id = std::get<1>(t); + int64_t p = std::get<2>(t); + if(p == 0) + { + l.parent_id = std::nullopt; + } + else + { + l.parent_id = p; + } + l.name = std::get<3>(t); + l.url = std::get<4>(t); + l.description = std::get<5>(t); + l.visibility = static_cast(std::get<6>(t)); + l.time = mw::secondsToTime(std::get<7>(t)); + return l; +} } // namespace @@ -18,7 +44,7 @@ DataSourceSQLite::fromFile(const std::string& db_file) { auto data_source = std::make_unique(); ASSIGN_OR_RETURN(data_source->db, mw::SQLite::connectFile(db_file)); - + DO_OR_RETURN(data_source->db->execute("PRAGMA foreign_keys = ON;")); // Perform schema upgrade here. // // data_source->upgradeSchema1To2(); @@ -30,11 +56,12 @@ DataSourceSQLite::fromFile(const std::string& db_file) "(id INTEGER PRIMARY KEY, openid_uid TEXT, name TEXT);")); DO_OR_RETURN(data_source->db->execute( "CREATE TABLE IF NOT EXISTS LinkItems " - "(id INTEGER PRIMARY KEY," - " FOREIGN KEY(owner_id) REFERENCES Users(id) NOT NULL," - " FOREIGN KEY(parent_id) REFERENCES LinkItems(id)," + "(id INTEGER PRIMARY KEY, owner_id INTEGER NOT NULL," + " parent_id INTEGER," " name TEXT NOT NULL, url TEXT, description TEXT," - " visibility INTEGER NOT NULL, time INTEGER NOT NULL);")); + " visibility INTEGER NOT NULL, time INTEGER NOT NULL," + " FOREIGN KEY(owner_id) REFERENCES Users(id)," + " FOREIGN KEY(parent_id) REFERENCES LinkItems(id));")); return data_source; } @@ -52,3 +79,54 @@ mw::E DataSourceSQLite::setSchemaVersion(int64_t v) const { return db->execute(std::format("PRAGMA user_version = {};", v)); } + +mw::E> +DataSourceSQLite::items(std::optional parent) +{ + std::vector links; + if(!parent.has_value()) + { + // Querying root-level links. + ASSIGN_OR_RETURN(links, (db->eval( + "SELECT id, owner_id, parent_id, name, url, description, visibility," + " time from LinkItems WHERE parent_id IS NULL;"))); + } + else + { + ASSIGN_OR_RETURN(mw::SQLiteStatement stat, db->statementFromStr( + "SELECT id, owner_id, parent_id, name, url, description, visibility," + " time from LinkItems WHERE parent_id = ?;")); + DO_OR_RETURN(stat.bind(*parent)); + ASSIGN_OR_RETURN(links, (db->eval(std::move(stat)))); + } + std::vector result; + for(const LinkTuple& t : links) + { + result.push_back(linkFromTuple(t)); + } + return result; +} + +mw::E DataSourceSQLite::addLink(LinkItem&& link) +{ + ASSIGN_OR_RETURN(mw::SQLiteStatement sql, db->statementFromStr( + "INSERT INTO LinkItems (id, owner_id, parent_id, name, url," + " description, visibility, time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")); + if(link.parent_id.has_value()) + { + DO_OR_RETURN(sql.bind( + std::nullopt, link.owner_id, *link.parent_id, link.name, link.url, + link.description, static_cast(link.visibility), + mw::timeToSeconds(mw::Clock::now()))); + } + else + { + DO_OR_RETURN(sql.bind( + std::nullopt, link.owner_id, std::nullopt, link.name, link.url, + link.description, static_cast(link.visibility), + mw::timeToSeconds(mw::Clock::now()))); + } + + DO_OR_RETURN(db->execute(std::move(sql))); + return db->lastInsertRowID(); +} diff --git a/src/data.hpp b/src/data.hpp index dc64e6f..006f1f2 100644 --- a/src/data.hpp +++ b/src/data.hpp @@ -43,7 +43,8 @@ public: // help with database migration. virtual mw::E getSchemaVersion() const = 0; - virtual std::vector items(std::optional parent) = 0; + virtual mw::E> items(std::optional parent) = 0; + virtual mw::E addLink(LinkItem&& link) = 0; protected: virtual mw::E setSchemaVersion(int64_t v) const = 0; @@ -62,6 +63,9 @@ public: mw::E getSchemaVersion() const override; + mw::E> items(std::optional parent) override; + mw::E addLink(LinkItem&& link) override; + // Do not use. DataSourceSQLite() = default; diff --git a/src/data_test.cpp b/src/data_test.cpp index f1a1a1c..633a102 100644 --- a/src/data_test.cpp +++ b/src/data_test.cpp @@ -10,8 +10,32 @@ using ::testing::IsEmpty; -TEST(DataSource, CanAddAndDeleteLink) +TEST(DataSource, CanAddLink) { ASSIGN_OR_FAIL(std::unique_ptr data, DataSourceSQLite::newFromMemory()); + LinkItem l0; + l0.owner_id = 1; + l0.parent_id = std::nullopt; + l0.name = "aaa"; + l0.url = "bbb"; + l0.description = "ccc"; + l0.visibility = LinkItem::PUBLIC; + + LinkItem l1; + l1.owner_id = 1; + l1.parent_id = 1; + l1.name = "ddd"; + l1.url = "eee"; + l1.description = "fff"; + l1.visibility = LinkItem::PRIVATE; + + ASSIGN_OR_FAIL(int64_t l0id, data->addLink(std::move(l0))); + EXPECT_EQ(l0id, 1); + ASSIGN_OR_FAIL(int64_t l1id, data->addLink(std::move(l1))); + EXPECT_EQ(l1id, 2); + ASSIGN_OR_FAIL(auto ls, data->items(1)); + ASSERT_EQ(ls.size(), 1); + EXPECT_EQ(ls[0].parent_id, 1); + EXPECT_EQ(ls[0].visibility, LinkItem::PRIVATE); } -- cgit v1.2.3-70-g09d2