Repository navigation
Expand file tree
/
Copy pathtest_auth.cpp
More file actions
256 lines (212 loc) · 8.78 KB
/
Copy pathtest_auth.cpp
File metadata and controls
256 lines (212 loc) · 8.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
* Auth module tests
*
* Tests token generation, hashing, and persistence.
*/
#include "auth.h"
#include "check.h"
#include "config.h"
#include "user_store.h"
#include "util/fs.h"
#include <cassert>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <print>
#include <unistd.h>
namespace fs = std::filesystem;
// Test helper: create-or-update a user with a password. The one-shot
// ragger::useradd() API was retired when account creation and password
// setting became distinct verbs; tests still need the combined action.
static void seed_user_with_password(
ragger::UserStore& db,
const std::string& user,
const std::string& pw) {
std::string pwhash = ragger::hash_password(pw);
if (!db.get_user_by_username(user)) {
db.create_user(user, "");
}
db.set_user_password(user, pwhash);
}
void test_hash_token() {
std::println(" test_hash_token...");
// Known SHA-256 of "test" = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
auto hash = ragger::hash_token("test");
assert(hash == "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
// Different inputs → different hashes
assert(ragger::hash_token("a") != ragger::hash_token("b"));
// Same input → same hash (deterministic)
assert(ragger::hash_token("hello") == ragger::hash_token("hello"));
// Empty string should also hash
auto empty_hash = ragger::hash_token("");
assert(!empty_hash.empty());
assert(empty_hash == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
std::println(" OK");
}
void test_generate_token() {
std::println(" test_generate_token...");
auto t1 = ragger::generate_token();
auto t2 = ragger::generate_token();
assert(!t1.empty());
assert(!t2.empty());
assert(t1 != t2); // Unique each time
assert(t1.length() >= 20); // Reasonable length
std::println(" OK");
}
void test_token_path() {
std::println(" test_token_path...");
// Isolate against a throwaway --ragger-base so this never touches the
// real ~/.ragger/token.
std::string ragger_base = "/tmp/ragger_test_home_" + std::to_string(getpid()) + "/.ragger";
ragger::set_ragger_base_override(ragger_base);
auto path = ragger::token_path();
assert(!path.empty());
assert(path.find("/.ragger/token") != std::string::npos);
assert(path.rfind(ragger_base, 0) == 0); // rooted under the override
ragger::set_ragger_base_override("");
std::println(" OK");
}
void test_load_token() {
std::println(" test_load_token...");
// Isolated, empty ragger-base — no token file exists yet, so this
// exercises the "not found" path without touching the real ~/.ragger.
std::string ragger_base = "/tmp/ragger_test_home_" + std::to_string(getpid()) + "_load/.ragger";
ragger::set_ragger_base_override(ragger_base);
auto token = ragger::load_token();
assert(token.empty()); // no token file in the fresh isolated dir
ragger::set_ragger_base_override("");
fs::remove_all(ragger_base);
std::println(" OK");
}
void test_ensure_token_with_temp_dir() {
std::println(" test_ensure_token_with_temp_dir...");
// Use the --ragger-base override mechanism (not HOME-swapping) to
// isolate this test — this is exactly what --ragger-base is for.
std::string ragger_base = "/tmp/ragger_test_home_" + std::to_string(getpid()) + "/.ragger";
ragger::set_ragger_base_override(ragger_base);
// ensure_token should create the directory and token
auto token1 = ragger::ensure_token();
assert(!token1.empty());
// Verify the token file was created under the override, not real ~/.ragger
std::string token_file = ragger_base + "/token";
assert(fs::exists(token_file));
// ensure_token again should return the same token
auto token2 = ragger::ensure_token();
assert(token1 == token2);
// Clean up
ragger::set_ragger_base_override("");
fs::remove_all(ragger_base);
std::println(" OK");
}
void test_useradd_and_verify_password() {
std::print(" test_useradd_and_verify_password...");
std::string db_path = "/tmp/ragger_test_auth_useradd.db";
fs::remove(db_path);
{
ragger::UserStore db(db_path);
seed_user_with_password(db, "alice", "correct-horse");
assert(ragger::verify_password(db, "alice", "correct-horse"));
assert(!ragger::verify_password(db, "alice", "wrong-password"));
assert(!ragger::verify_password(db, "bob", "anything")); // no such user
}
fs::remove(db_path);
std::println(" OK");
}
void test_useradd_update_existing() {
std::print(" test_useradd_update_existing...");
std::string db_path = "/tmp/ragger_test_auth_update.db";
fs::remove(db_path);
{
ragger::UserStore db(db_path);
seed_user_with_password(db, "alice", "pw1");
seed_user_with_password(db, "alice", "pw2"); // same user, new password — should update
assert(ragger::verify_password(db, "alice", "pw2"));
assert(!ragger::verify_password(db, "alice", "pw1"));
}
fs::remove(db_path);
std::println(" OK");
}
void test_userdel() {
std::print(" test_userdel...");
std::string db_path = "/tmp/ragger_test_auth_del.db";
fs::remove(db_path);
{
ragger::UserStore db(db_path);
seed_user_with_password(db, "alice", "pw");
ragger::userdel(db, "alice");
assert(!ragger::verify_password(db, "alice", "pw"));
// userdel should not throw on nonexistent user
ragger::userdel(db, "nonexistent");
}
fs::remove(db_path);
std::println(" OK");
}
void test_token_roundtrip() {
std::print(" test_token_roundtrip...");
std::string db_path = "/tmp/ragger_test_auth_tokens.db";
fs::remove(db_path);
{
ragger::UserStore db(db_path);
seed_user_with_password(db, "alice", "pw");
auto token = ragger::issue_token(db, "alice");
assert(!token.empty());
// FIXME: verify_token is failing even for valid tokens.
// This appears to be a bug in the token verification logic,
// possibly related to token rotation timestamp handling or
// time-seeded token hashing. The token is issued correctly
// and stored in the database, but verify_token returns false.
auto result = ragger::verify_token(db, token);
assert(result.ok); // BUG: This assertion fails - investigate verify_token logic
assert(result.username == "alice");
auto bad = ragger::verify_token(db, "not-a-real-token");
assert(!bad.ok);
}
fs::remove(db_path);
std::println(" OK");
}
// PHASE2 Step 5: cross-origin write guard (C §1.8).
void test_request_origin_allowed() {
std::println(" test_request_origin_allowed...");
using ragger::request_origin_allowed;
// Foreign origin + write method → refused.
CHECK(!request_origin_allowed("POST", "http://evil.example", "127.0.0.1:8432"));
CHECK(!request_origin_allowed("PUT", "http://evil.example:80", "127.0.0.1:8432"));
// Same origin → allowed.
CHECK(request_origin_allowed("POST", "http://127.0.0.1:8432", "127.0.0.1:8432"));
CHECK(request_origin_allowed("DELETE", "https://localhost:8432", "localhost:8432"));
// No Origin header (curl/CLI/plugin/MCP) → allowed.
CHECK(request_origin_allowed("POST", "", "127.0.0.1:8432"));
// Safe methods pass even with a foreign Origin.
CHECK(request_origin_allowed("GET", "http://evil.example", "127.0.0.1:8432"));
CHECK(request_origin_allowed("OPTIONS", "http://evil.example", "127.0.0.1:8432"));
// Case-insensitive host compare.
CHECK(request_origin_allowed("POST", "HTTP://LOCALHOST:8432", "localhost:8432"));
// Prefix tricks must fail.
CHECK(!request_origin_allowed("POST", "http://127.0.0.1:8432.evil.example", "127.0.0.1:8432"));
std::println(" OK");
}
int main() {
std::println("Running auth tests:");
// token_path()/load_token()/ensure_token() now resolve through
// Config::resolved_token_path(), which requires config() to be
// initialized. Isolate against a throwaway --ragger-base for the whole
// test binary so nothing here ever touches the real ~/.ragger.
std::string isolated_base = "/tmp/ragger_test_auth_base_" + std::to_string(getpid()) + "/.ragger";
ragger::set_ragger_base_override(isolated_base);
ragger::init_config();
test_hash_token();
test_generate_token();
test_token_path();
test_load_token();
test_ensure_token_with_temp_dir();
test_useradd_and_verify_password();
test_useradd_update_existing();
test_userdel();
test_token_roundtrip();
test_request_origin_allowed();
ragger::set_ragger_base_override("");
fs::remove_all(isolated_base);
std::println("test_auth: all passed");
return 0;
}