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 argon2::{PasswordHash, PasswordVerifier};
23

            
24
use hnefatafl_copenhagen::accounts::{Account, Accounts};
25

            
26
use super::*;
27

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

            
33
const ADDRESS: &str = "localhost:49152";
34

            
35
struct Server(Child);
36

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

            
55
3
        Ok(Server(server))
56
3
    }
57
}
58

            
59
impl Drop for Server {
60
3
    fn drop(&mut self) {
61
3
        self.0.kill().unwrap();
62
3
    }
63
}
64

            
65
#[test]
66
3
fn capital_letters_fail() {
67
3
    let mut accounts = Accounts::default();
68

            
69
3
    let password = "A".to_string();
70
3
    let ctx = Argon2::default();
71
3
    let password_hash = ctx.hash_password(password.as_bytes()).unwrap().to_string();
72

            
73
3
    let account = Account {
74
3
        password: password_hash,
75
3
        logged_in: Some(0),
76
3
        ..Default::default()
77
3
    };
78

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

            
84
3
        account.password = password_hash;
85
3
    }
86

            
87
    {
88
3
        let account = accounts.0.get_mut("testing").unwrap();
89
3
        let hash = PasswordHash::try_from(account.password.as_str()).unwrap();
90

            
91
3
        assert!(
92
3
            Argon2::default()
93
3
                .verify_password(password.as_bytes(), &hash)
94
3
                .is_ok()
95
        );
96
    }
97
3
}
98

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

            
107
3
    let _server = Server::new(false);
108
3
    thread::sleep(Duration::from_millis(10));
109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
182
2
    Ok(())
183
3
}
184

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

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

            
202
    let t0 = Instant::now();
203

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

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

            
214
            reader.read_line(&mut buf).unwrap();
215
            buf.clear();
216
        }));
217
    }
218

            
219
    for handle in handles {
220
        handle.join().unwrap();
221
    }
222

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

            
226
    Ok(())
227
}