1
// This file is part of hnefatafl-copenhagen.
2
//
3
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU Affero General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU Affero General Public License for more details.
12
//
13
// You should have received a copy of the GNU Affero General Public License
14
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
//
16
// SPDX-License-Identifier: AGPL-3.0-or-later
17
// SPDX-FileCopyrightText: 2025 David Campbell <david@hnefatafl.org>
18

            
19
#![allow(clippy::unwrap_used)]
20
#![cfg(test)]
21

            
22
use crate::Server as ServerFull;
23

            
24
use argon2::{PasswordHash, PasswordVerifier};
25

            
26
use hnefatafl_copenhagen::accounts::{Account, Accounts};
27

            
28
use super::*;
29

            
30
use std::net::TcpStream;
31
use std::process::{Child, Stdio};
32
use std::thread;
33
use std::time::{Duration, Instant};
34

            
35
const ADDRESS: &str = "localhost:49152";
36

            
37
struct Server(Child);
38

            
39
impl Server {
40
2
    fn new(release: bool) -> anyhow::Result<Server> {
41
2
        let server = if release {
42
            std::process::Command::new("./target/release/hnefatafl-server-full")
43
                .stdout(Stdio::null())
44
                .stderr(Stdio::null())
45
                .arg("--skip-the-data-file")
46
                .arg("--skip-advertising-updates")
47
                .spawn()?
48
        } else {
49
2
            std::process::Command::new("./target/debug/hnefatafl-server-full")
50
2
                .stdout(Stdio::null())
51
2
                .stderr(Stdio::null())
52
2
                .arg("--skip-the-data-file")
53
2
                .arg("--skip-advertising-updates")
54
2
                .spawn()?
55
        };
56

            
57
2
        Ok(Server(server))
58
2
    }
59
}
60

            
61
impl Drop for Server {
62
2
    fn drop(&mut self) {
63
2
        self.0.kill().unwrap();
64
2
    }
65
}
66

            
67
#[test]
68
2
fn capital_letters_fail() {
69
2
    let mut accounts = Accounts::default();
70

            
71
2
    let password = "A".to_string();
72
2
    let ctx = Argon2::default();
73
2
    let password_hash = ctx.hash_password(password.as_bytes()).unwrap().to_string();
74

            
75
2
    let account = Account {
76
2
        password: password_hash,
77
2
        logged_in: Some(0),
78
2
        ..Default::default()
79
2
    };
80

            
81
2
    accounts.0.insert("testing".to_string(), account);
82
2
    {
83
2
        let account = accounts.0.get_mut("testing").unwrap();
84
2
        let password_hash = ctx.hash_password(password.as_bytes()).unwrap().to_string();
85
2

            
86
2
        account.password = password_hash;
87
2
    }
88

            
89
    {
90
2
        let account = accounts.0.get_mut("testing").unwrap();
91
2
        let hash = PasswordHash::try_from(account.password.as_str()).unwrap();
92

            
93
2
        assert!(
94
2
            Argon2::default()
95
2
                .verify_password(password.as_bytes(), &hash)
96
2
                .is_ok()
97
        );
98
    }
99
2
}
100

            
101
#[test]
102
2
fn server_full() -> anyhow::Result<()> {
103
2
    std::process::Command::new("cargo")
104
2
        .arg("build")
105
2
        .arg("--bin")
106
2
        .arg("hnefatafl-server-full")
107
2
        .output()?;
108

            
109
2
    let _server = Server::new(false);
110
2
    thread::sleep(Duration::from_millis(10));
111

            
112
2
    let mut buf = String::new();
113
2
    let mut socket_1 = TcpStream::connect(ADDRESS)?;
114
2
    let mut reader_1 = BufReader::new(socket_1.try_clone()?);
115

            
116
2
    socket_1.write_all(format!("{VERSION_ID} create_account player-1\n").as_bytes())?;
117
2
    reader_1.read_line(&mut buf)?;
118
2
    assert_eq!(buf, "= login\n");
119
2
    buf.clear();
120

            
121
2
    socket_1.write_all(b"change_password\n")?;
122
2
    reader_1.read_line(&mut buf)?;
123
2
    assert_eq!(buf, "= change_password\n");
124
2
    buf.clear();
125

            
126
2
    socket_1.write_all(b"new_game attacker rated fischer 900000 10 11\n")?;
127
2
    reader_1.read_line(&mut buf)?;
128
2
    assert_eq!(
129
        buf,
130
        "= new_game game 0 player-1 _ rated fischer 900000 10 11 _ false {}\n"
131
    );
132
2
    buf.clear();
133

            
134
2
    let mut socket_2 = TcpStream::connect(ADDRESS)?;
135
2
    let mut reader_2 = BufReader::new(socket_2.try_clone()?);
136

            
137
2
    socket_2.write_all(format!("{VERSION_ID} create_account player-2\n").as_bytes())?;
138
2
    reader_2.read_line(&mut buf)?;
139
2
    assert_eq!(buf, "= login\n");
140
2
    buf.clear();
141

            
142
2
    socket_2.write_all(b"join_game_pending 0\n")?;
143
2
    reader_2.read_line(&mut buf)?;
144
2
    assert_eq!(buf, "= join_game_pending 0\n");
145
2
    buf.clear();
146

            
147
2
    reader_1.read_line(&mut buf)?;
148
2
    assert_eq!(buf, "= challenge_requested 0\n");
149
2
    buf.clear();
150

            
151
    // Fixme: "join_game_pending 0\n" should not be allowed!
152
2
    socket_1.write_all(b"join_game 0\n")?;
153
2
    reader_1.read_line(&mut buf)?;
154
2
    assert_eq!(
155
        buf,
156
        "= join_game player-1 player-2 rated fischer 900000 10 11\n"
157
    );
158
2
    buf.clear();
159

            
160
2
    reader_2.read_line(&mut buf)?;
161
2
    assert_eq!(
162
        buf,
163
        "= join_game player-1 player-2 rated fischer 900000 10 11\n"
164
    );
165
2
    buf.clear();
166

            
167
2
    reader_1.read_line(&mut buf)?;
168
2
    assert_eq!(buf, "game 0 generate_move attacker\n");
169
2
    buf.clear();
170

            
171
2
    socket_1.write_all(b"game 0 play attacker resigns _\n")?;
172
2
    reader_1.read_line(&mut buf)?;
173
2
    assert_eq!(buf, "= game_over 0 defender_wins\n");
174
2
    buf.clear();
175

            
176
2
    reader_2.read_line(&mut buf)?;
177
2
    assert_eq!(buf, "game 0 play attacker resigns \n");
178
2
    buf.clear();
179

            
180
2
    reader_2.read_line(&mut buf)?;
181
2
    assert_eq!(buf, "= game_over 0 defender_wins\n");
182
2
    buf.clear();
183

            
184
2
    Ok(())
185
2
}
186

            
187
// echo "* soft nofile 1000000" >> /etc/security/limits.conf
188
// echo "* hard nofile 1000000" >> /etc/security/limits.conf
189
// fish
190
// ulimit --file-descriptor-count 1000000
191
#[ignore = "too slow, too many tcp connections"]
192
#[test]
193
fn many_clients() -> anyhow::Result<()> {
194
    std::process::Command::new("cargo")
195
        .arg("build")
196
        .arg("--release")
197
        .arg("--bin")
198
        .arg("hnefatafl-server-full")
199
        .output()?;
200

            
201
    let _server = Server::new(true);
202
    thread::sleep(Duration::from_millis(10));
203

            
204
    let t0 = Instant::now();
205

            
206
    let mut handles = Vec::new();
207
    for i in 0..1_000 {
208
        handles.push(thread::spawn(move || {
209
            let mut buf = String::new();
210
            let mut tcp = TcpStream::connect(ADDRESS).unwrap();
211
            let mut reader = BufReader::new(tcp.try_clone().unwrap());
212

            
213
            tcp.write_all(format!("{VERSION_ID} create_account q-player-{i}\n").as_bytes())
214
                .unwrap();
215

            
216
            reader.read_line(&mut buf).unwrap();
217
            buf.clear();
218
        }));
219
    }
220

            
221
    for handle in handles {
222
        handle.join().unwrap();
223
    }
224

            
225
    let t1 = Instant::now();
226
    println!("many clients: {:?}", t1 - t0);
227

            
228
    Ok(())
229
}
230

            
231
10
fn create_account(server: &mut ServerFull, tx: Sender<String>) -> anyhow::Result<()> {
232
10
    if let Some((_, bool, message)) =
233
10
        server.handle_messages_internal("0 david create_account PASSWORD", Some(tx))
234
    {
235
10
        assert!(bool);
236
10
        assert_eq!(message, "create_account");
237

            
238
10
        Ok(())
239
    } else {
240
        Err(anyhow::Error::msg("didn't get a response"))
241
    }
242
10
}
243

            
244
2
fn login(server: &mut ServerFull, tx: Sender<String>, password: &str) -> anyhow::Result<()> {
245
2
    if let Some((_, bool, message)) =
246
2
        server.handle_messages_internal(&format!("0 david login {password}"), Some(tx))
247
    {
248
2
        assert!(bool);
249
2
        assert_eq!(message, "login");
250

            
251
2
        Ok(())
252
    } else {
253
        Err(anyhow::Error::msg("didn't get a response"))
254
    }
255
2
}
256

            
257
#[test]
258
2
fn admin() -> anyhow::Result<()> {
259
2
    let mut server = ServerFull {
260
2
        ..ServerFull::default()
261
2
    };
262

            
263
2
    let (tx, rx) = mpsc::channel();
264
2
    create_account(&mut server, tx)?;
265

            
266
2
    assert!(
267
2
        server
268
2
            .handle_messages_internal("0 david admin", None)
269
2
            .is_none()
270
    );
271

            
272
2
    server.admins.insert("david".to_string());
273
2
    assert!(
274
2
        server
275
2
            .handle_messages_internal("0 david admin", None)
276
2
            .is_none()
277
    );
278
2
    assert_eq!(Ok("= admin".to_string()), rx.recv());
279

            
280
2
    Ok(())
281
2
}
282

            
283
#[test]
284
2
fn admin_tournament() -> anyhow::Result<()> {
285
2
    let mut server = ServerFull {
286
2
        ..ServerFull::default()
287
2
    };
288

            
289
2
    let (tx, rx) = mpsc::channel();
290
2
    create_account(&mut server, tx)?;
291

            
292
2
    assert!(
293
2
        server
294
2
            .handle_messages_internal("0 david admin_tournament", None)
295
2
            .is_none()
296
    );
297

            
298
2
    server.admins_tournament.insert("david".to_string());
299
2
    assert!(
300
2
        server
301
2
            .handle_messages_internal("0 david admin_tournament", None)
302
2
            .is_none()
303
    );
304
2
    assert_eq!(Ok("= admin_tournament".to_string()), rx.recv());
305

            
306
2
    Ok(())
307
2
}
308

            
309
#[test]
310
2
fn archived_games() -> anyhow::Result<()> {
311
2
    let mut server = ServerFull {
312
2
        ..ServerFull::default()
313
2
    };
314

            
315
2
    let (tx, rx) = mpsc::channel();
316
2
    create_account(&mut server, tx)?;
317

            
318
2
    assert!(
319
2
        server
320
2
            .handle_messages_internal("0 david archived_games", None)
321
2
            .is_none()
322
    );
323

            
324
2
    assert_eq!(Ok("= archived_games".to_string()), rx.recv());
325
2
    assert_eq!(Ok("[]".to_string()), rx.recv());
326

            
327
2
    Ok(())
328
2
}
329

            
330
#[test]
331
2
fn change_password() -> anyhow::Result<()> {
332
2
    let mut server = ServerFull {
333
2
        ..ServerFull::default()
334
2
    };
335

            
336
2
    let (tx, _rx) = mpsc::channel();
337
2
    create_account(&mut server, tx.clone())?;
338

            
339
2
    let option = server.handle_messages_internal("0 david change_password password", None);
340
2
    assert!(option.is_some());
341
2
    if let Some((_, bool, message)) = option {
342
2
        assert!(bool);
343
2
        assert_eq!(message, "change_password");
344
    }
345

            
346
2
    server.handle_messages_internal("0 david logout", None);
347
2
    login(&mut server, tx, "password")?;
348

            
349
2
    Ok(())
350
2
}
351

            
352
#[test]
353
#[allow(clippy::float_cmp)]
354
1
fn check_update_rd() -> anyhow::Result<()> {
355
1
    let mut server = ServerFull {
356
1
        ..ServerFull::default()
357
1
    };
358

            
359
1
    let (tx, _rx) = mpsc::channel();
360
1
    create_account(&mut server, tx)?;
361

            
362
1
    if let Some(account) = server.accounts.0.get_mut("david") {
363
1
        account.rating.rd = 100.0;
364
1
    }
365

            
366
1
    assert!(!server.check_update_rd());
367
1
    server.ran_update_rd.0 = Timestamp::now() - TWO_MONTHS_MICRO_SECONDS.microseconds();
368
1
    assert!(server.check_update_rd());
369

            
370
1
    if let Some(account) = server.accounts.0.get_mut("david") {
371
1
        assert_eq!(118.0, account.rating.rd.round_ties_even());
372
    }
373

            
374
1
    Ok(())
375
1
}