aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMetroWind <chris.corsair@gmail.com>2025-09-12 14:41:42 -0700
committerMetroWind <chris.corsair@gmail.com>2025-09-12 14:41:42 -0700
commiteedd1c6b6a612daeb0e4e154bc0200df6826aa1d (patch)
tree88f9d49e8bb6bca54b058a1b3763c1e86e8f68a8
parentc33272456bf969aa47bca432ef302530aa2cf752 (diff)
Implement items() and addLink() in data source.
-rw-r--r--src/app.hpp4
-rw-r--r--src/app_test.cpp217
-rw-r--r--src/data.cpp88
-rw-r--r--src/data.hpp6
-rw-r--r--src/data_test.cpp26
5 files changed, 155 insertions, 186 deletions
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:
27 std::string urlFor(const std::string& name, const std::string& arg="") 27 std::string urlFor(const std::string& name, const std::string& arg="")
28 const; 28 const;
29 29
30 void handleIndex(Response& res) const;
31 void handleLogin(Response& res) const;
32 void handleOpenIDRedirect(const Request& req, Response& res) const;
33
30private: 34private:
31 void setup() override; 35 void setup() override;
32 36
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;
19using ::testing::FieldsAre; 19using ::testing::FieldsAre;
20using ::testing::ContainsRegex; 20using ::testing::ContainsRegex;
21 21
22void PrintTo(const ShortLink& link, std::ostream* os) 22// class UserAppTest : public testing::Test
23{ 23// {
24 *os << "ShortLink(id: " << link.id 24// protected:
25 << ", shortcut: " << link.shortcut 25// UserAppTest()
26 << ", original_url: " << link.original_url 26// {
27 << ", type: " << link.type 27// config.base_url = "http://localhost:8080/";
28 << ", user_id: " << link.user_id 28// config.listen_address = "localhost";
29 << ", user_name: " << link.user_name 29// config.listen_port = 8080;
30 << ", visits: " << link.visits 30// config.data_dir = ".";
31 << ", time_creation: " << link.time_creation << ")";
32}
33 31
34class UserAppTest : public testing::Test 32// auto auth = std::make_unique<mw::AuthMock>();
35{
36protected:
37 UserAppTest()
38 {
39 config.base_url = "http://localhost:8080/";
40 config.listen_address = "localhost";
41 config.listen_port = 8080;
42 config.data_dir = ".";
43 33
44 auto auth = std::make_unique<mw::AuthMock>(); 34// mw::UserInfo expected_user;
35// expected_user.name = "mw";
36// expected_user.id = "mw";
37// mw::Tokens token;
38// token.access_token = "aaa";
39// EXPECT_CALL(*auth, getUser(std::move(token)))
40// .Times(::testing::AtLeast(0))
41// .WillRepeatedly(Return(expected_user));
42// auto data = std::make_unique<DataSourceMock>();
43// data_source = data.get();
45 44
46 mw::UserInfo expected_user; 45// app = std::make_unique<App>(config, std::move(data), std::move(auth));
47 expected_user.name = "mw"; 46// }
48 expected_user.id = "mw";
49 mw::Tokens token;
50 token.access_token = "aaa";
51 EXPECT_CALL(*auth, getUser(std::move(token)))
52 .Times(::testing::AtLeast(0))
53 .WillRepeatedly(Return(expected_user));
54 auto data = std::make_unique<DataSourceMock>();
55 data_source = data.get();
56 47
57 app = std::make_unique<App>(config, std::move(data), std::move(auth)); 48// Configuration config;
58 } 49// std::unique_ptr<App> app;
50// const DataSourceMock* data_source;
51// };
59 52
60 Configuration config; 53// TEST_F(UserAppTest, CanDenyAccessToLinkList)
61 std::unique_ptr<App> app; 54// {
62 const DataSourceMock* data_source; 55// EXPECT_TRUE(mw::isExpected(app->start()));
63}; 56// {
64 57// mw::HTTPSession client;
65TEST_F(UserAppTest, CanDenyAccessToLinkList) 58// ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get(
66{ 59// mw::HTTPRequest("http://localhost:8080/_/links")));
67 EXPECT_TRUE(mw::isExpected(app->start())); 60// EXPECT_EQ(res->status, 401);
68 { 61// }
69 mw::HTTPSession client; 62// app->stop();
70 ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get( 63// app->wait();
71 mw::HTTPRequest("http://localhost:8080/_/links"))); 64// }
72 EXPECT_EQ(res->status, 401);
73 }
74 app->stop();
75 app->wait();
76}
77
78TEST_F(UserAppTest, CanHandleLinkList)
79{
80 std::vector<ShortLink> links;
81 ShortLink link0;
82 link0.shortcut = "link0";
83 link0.original_url = "a";
84 link0.id = 1;
85 link0.user_id = "mw";
86 link0.user_name = "mw";
87 link0.type = ShortLink::NORMAL;
88 ShortLink link1;
89 link1.shortcut = "link1";
90 link1.original_url = "b";
91 link1.id = 2;
92 link1.user_id = "mw";
93 link1.user_name = "mw";
94 link1.type = ShortLink::REGEXP;
95 links.push_back(std::move(link0));
96 links.push_back(std::move(link1));
97
98 EXPECT_CALL(*data_source, getAllLinks("mw"))
99 .WillOnce(Return(std::move(links)));
100
101 EXPECT_TRUE(mw::isExpected(app->start()));
102 {
103 mw::HTTPSession client;
104 ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get(
105 mw::HTTPRequest("http://localhost:8080/_/links")
106 .addHeader("Cookie", "shrt-access-token=aaa")));
107 EXPECT_EQ(res->status, 200) << "Response body: " << res->payloadAsStr();
108 EXPECT_THAT(res->payloadAsStr(), ContainsRegex("<td>a</td>[[:space:]]*<td>-</td>"));
109 EXPECT_THAT(res->payloadAsStr(), ContainsRegex("<td>b</td>[[:space:]]*<td>✅</td>"));
110 }
111 app->stop();
112 app->wait();
113}
114
115TEST_F(UserAppTest, CanDenyAccessToNewLink)
116{
117 EXPECT_TRUE(mw::isExpected(app->start()));
118 {
119 mw::HTTPSession client;
120 ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get(
121 mw::HTTPRequest("http://localhost:8080/_/new-link")));
122 EXPECT_EQ(res->status, 401);
123 }
124 app->stop();
125 app->wait();
126}
127
128TEST_F(UserAppTest, CanHandleNewLink)
129{
130 EXPECT_TRUE(mw::isExpected(app->start()));
131 {
132 mw::HTTPSession client;
133 ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.get(
134 mw::HTTPRequest("http://localhost:8080/_/new-link")
135 .addHeader("Cookie", "shrt-access-token=aaa")));
136 EXPECT_EQ(res->status, 200);
137 EXPECT_THAT(res->payloadAsStr(), HasSubstr("Create a New Link"));
138 }
139 app->stop();
140 app->wait();
141}
142
143TEST_F(UserAppTest, CanDenyAccessToCreateLink)
144{
145 EXPECT_TRUE(mw::isExpected(app->start()));
146 {
147 mw::HTTPSession client;
148 ASSIGN_OR_FAIL(const mw::HTTPResponse* res, client.post(
149 mw::HTTPRequest("http://localhost:8080/_/create-link")));
150 EXPECT_EQ(res->status, 401);
151 }
152 app->stop();
153 app->wait();
154}
155
156TEST_F(UserAppTest, CanDenyHandleCreateLink)
157{
158 EXPECT_CALL(*data_source, addLink(
159 FieldsAre(
160 _, // id
161 "abc", // shortcut
162 "http://darksair.org", // original_url
163 ShortLink::NORMAL, // type
164 "mw", // user_id
165 "", // user_name
166 _, // visits
167 _))) // time_creation
168 .WillOnce(Return(mw::E<void>()));
169
170 EXPECT_CALL(*data_source, addLink(
171 FieldsAre(
172 _, // id
173 "xyz", // shortcut
174 "http://mws.rocks", // original_url
175 ShortLink::REGEXP, // type
176 "mw", // user_id
177 "", // user_name
178 _, // visits
179 _))) // time_creation
180 .WillOnce(Return(mw::E<void>()));
181
182 EXPECT_TRUE(mw::isExpected(app->start()));
183 {
184 mw::HTTPSession client;
185 ASSIGN_OR_FAIL(const mw::HTTPResponse* res1, client.post(
186 mw::HTTPRequest("http://localhost:8080/_/create-link")
187 .setPayload("shortcut=abc&original_url=http%3A%2F%2Fdarksair%2Eorg"
188 "&regexp=off")
189 .addHeader("Cookie", "shrt-access-token=aaa")
190 .setContentType("application/x-www-form-urlencoded")));
191 EXPECT_EQ(res1->status, 302);
192 EXPECT_EQ(res1->header.at("Location"), "http://localhost:8080/");
193
194 ASSIGN_OR_FAIL(const mw::HTTPResponse* res2, client.post(
195 mw::HTTPRequest("http://localhost:8080/_/create-link")
196 .setPayload("shortcut=xyz&original_url=http%3A%2F%2Fmws%2Erocks"
197 "&regexp=on")
198 .addHeader("Cookie", "shrt-access-token=aaa")
199 .setContentType("application/x-www-form-urlencoded")));
200 EXPECT_EQ(res2->status, 302);
201 EXPECT_EQ(res2->header.at("Location"), "http://localhost:8080/");
202 }
203 app->stop();
204 app->wait();
205}
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 @@
1#include <format>
1#include <memory> 2#include <memory>
2#include <string> 3#include <string>
3#include <expected> 4#include <expected>
@@ -8,8 +9,33 @@
8 9
9#include "data.hpp" 10#include "data.hpp"
10 11
12// id, owner_id, parent_id, name, url, desc, visibility, time.
13#define LINK_TUPLE_TYPES int64_t,int64_t,int64_t,std::string,std::string,std::string,int,int64_t
14
11namespace 15namespace
12{ 16{
17using LinkTuple = std::tuple<LINK_TUPLE_TYPES>;
18LinkItem linkFromTuple(const LinkTuple& t)
19{
20 LinkItem l;
21 l.id = std::get<0>(t);
22 l.owner_id = std::get<1>(t);
23 int64_t p = std::get<2>(t);
24 if(p == 0)
25 {
26 l.parent_id = std::nullopt;
27 }
28 else
29 {
30 l.parent_id = p;
31 }
32 l.name = std::get<3>(t);
33 l.url = std::get<4>(t);
34 l.description = std::get<5>(t);
35 l.visibility = static_cast<LinkItem::Visibility>(std::get<6>(t));
36 l.time = mw::secondsToTime(std::get<7>(t));
37 return l;
38}
13 39
14} // namespace 40} // namespace
15 41
@@ -18,7 +44,7 @@ DataSourceSQLite::fromFile(const std::string& db_file)
18{ 44{
19 auto data_source = std::make_unique<DataSourceSQLite>(); 45 auto data_source = std::make_unique<DataSourceSQLite>();
20 ASSIGN_OR_RETURN(data_source->db, mw::SQLite::connectFile(db_file)); 46 ASSIGN_OR_RETURN(data_source->db, mw::SQLite::connectFile(db_file));
21 47 DO_OR_RETURN(data_source->db->execute("PRAGMA foreign_keys = ON;"));
22 // Perform schema upgrade here. 48 // Perform schema upgrade here.
23 // 49 //
24 // data_source->upgradeSchema1To2(); 50 // data_source->upgradeSchema1To2();
@@ -30,11 +56,12 @@ DataSourceSQLite::fromFile(const std::string& db_file)
30 "(id INTEGER PRIMARY KEY, openid_uid TEXT, name TEXT);")); 56 "(id INTEGER PRIMARY KEY, openid_uid TEXT, name TEXT);"));
31 DO_OR_RETURN(data_source->db->execute( 57 DO_OR_RETURN(data_source->db->execute(
32 "CREATE TABLE IF NOT EXISTS LinkItems " 58 "CREATE TABLE IF NOT EXISTS LinkItems "
33 "(id INTEGER PRIMARY KEY," 59 "(id INTEGER PRIMARY KEY, owner_id INTEGER NOT NULL,"
34 " FOREIGN KEY(owner_id) REFERENCES Users(id) NOT NULL," 60 " parent_id INTEGER,"
35 " FOREIGN KEY(parent_id) REFERENCES LinkItems(id),"
36 " name TEXT NOT NULL, url TEXT, description TEXT," 61 " name TEXT NOT NULL, url TEXT, description TEXT,"
37 " visibility INTEGER NOT NULL, time INTEGER NOT NULL);")); 62 " visibility INTEGER NOT NULL, time INTEGER NOT NULL,"
63 " FOREIGN KEY(owner_id) REFERENCES Users(id),"
64 " FOREIGN KEY(parent_id) REFERENCES LinkItems(id));"));
38 return data_source; 65 return data_source;
39} 66}
40 67
@@ -52,3 +79,54 @@ mw::E<void> DataSourceSQLite::setSchemaVersion(int64_t v) const
52{ 79{
53 return db->execute(std::format("PRAGMA user_version = {};", v)); 80 return db->execute(std::format("PRAGMA user_version = {};", v));
54} 81}
82
83mw::E<std::vector<LinkItem>>
84DataSourceSQLite::items(std::optional<int64_t> parent)
85{
86 std::vector<LinkTuple> links;
87 if(!parent.has_value())
88 {
89 // Querying root-level links.
90 ASSIGN_OR_RETURN(links, (db->eval<LINK_TUPLE_TYPES>(
91 "SELECT id, owner_id, parent_id, name, url, description, visibility,"
92 " time from LinkItems WHERE parent_id IS NULL;")));
93 }
94 else
95 {
96 ASSIGN_OR_RETURN(mw::SQLiteStatement stat, db->statementFromStr(
97 "SELECT id, owner_id, parent_id, name, url, description, visibility,"
98 " time from LinkItems WHERE parent_id = ?;"));
99 DO_OR_RETURN(stat.bind<int64_t>(*parent));
100 ASSIGN_OR_RETURN(links, (db->eval<LINK_TUPLE_TYPES>(std::move(stat))));
101 }
102 std::vector<LinkItem> result;
103 for(const LinkTuple& t : links)
104 {
105 result.push_back(linkFromTuple(t));
106 }
107 return result;
108}
109
110mw::E<int64_t> DataSourceSQLite::addLink(LinkItem&& link)
111{
112 ASSIGN_OR_RETURN(mw::SQLiteStatement sql, db->statementFromStr(
113 "INSERT INTO LinkItems (id, owner_id, parent_id, name, url,"
114 " description, visibility, time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
115 if(link.parent_id.has_value())
116 {
117 DO_OR_RETURN(sql.bind(
118 std::nullopt, link.owner_id, *link.parent_id, link.name, link.url,
119 link.description, static_cast<int>(link.visibility),
120 mw::timeToSeconds(mw::Clock::now())));
121 }
122 else
123 {
124 DO_OR_RETURN(sql.bind(
125 std::nullopt, link.owner_id, std::nullopt, link.name, link.url,
126 link.description, static_cast<int>(link.visibility),
127 mw::timeToSeconds(mw::Clock::now())));
128 }
129
130 DO_OR_RETURN(db->execute(std::move(sql)));
131 return db->lastInsertRowID();
132}
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:
43 // help with database migration. 43 // help with database migration.
44 virtual mw::E<int64_t> getSchemaVersion() const = 0; 44 virtual mw::E<int64_t> getSchemaVersion() const = 0;
45 45
46 virtual std::vector<LinkItem> items(std::optional<int64_t> parent) = 0; 46 virtual mw::E<std::vector<LinkItem>> items(std::optional<int64_t> parent) = 0;
47 virtual mw::E<int64_t> addLink(LinkItem&& link) = 0;
47 48
48protected: 49protected:
49 virtual mw::E<void> setSchemaVersion(int64_t v) const = 0; 50 virtual mw::E<void> setSchemaVersion(int64_t v) const = 0;
@@ -62,6 +63,9 @@ public:
62 63
63 mw::E<int64_t> getSchemaVersion() const override; 64 mw::E<int64_t> getSchemaVersion() const override;
64 65
66 mw::E<std::vector<LinkItem>> items(std::optional<int64_t> parent) override;
67 mw::E<int64_t> addLink(LinkItem&& link) override;
68
65 // Do not use. 69 // Do not use.
66 DataSourceSQLite() = default; 70 DataSourceSQLite() = default;
67 71
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 @@
10 10
11using ::testing::IsEmpty; 11using ::testing::IsEmpty;
12 12
13TEST(DataSource, CanAddAndDeleteLink) 13TEST(DataSource, CanAddLink)
14{ 14{
15 ASSIGN_OR_FAIL(std::unique_ptr<DataSourceSQLite> data, 15 ASSIGN_OR_FAIL(std::unique_ptr<DataSourceSQLite> data,
16 DataSourceSQLite::newFromMemory()); 16 DataSourceSQLite::newFromMemory());
17 LinkItem l0;
18 l0.owner_id = 1;
19 l0.parent_id = std::nullopt;
20 l0.name = "aaa";
21 l0.url = "bbb";
22 l0.description = "ccc";
23 l0.visibility = LinkItem::PUBLIC;
24
25 LinkItem l1;
26 l1.owner_id = 1;
27 l1.parent_id = 1;
28 l1.name = "ddd";
29 l1.url = "eee";
30 l1.description = "fff";
31 l1.visibility = LinkItem::PRIVATE;
32
33 ASSIGN_OR_FAIL(int64_t l0id, data->addLink(std::move(l0)));
34 EXPECT_EQ(l0id, 1);
35 ASSIGN_OR_FAIL(int64_t l1id, data->addLink(std::move(l1)));
36 EXPECT_EQ(l1id, 2);
37 ASSIGN_OR_FAIL(auto ls, data->items(1));
38 ASSERT_EQ(ls.size(), 1);
39 EXPECT_EQ(ls[0].parent_id, 1);
40 EXPECT_EQ(ls[0].visibility, LinkItem::PRIVATE);
17} 41}