| 1 | // vtest vflags: -prod |
| 2 | // vtest build: !windows // fasthttp.Server.run is not implemented on windows yet |
| 3 | import veb |
| 4 | import net.http |
| 5 | import os |
| 6 | import time |
| 7 | |
| 8 | const port = 13003 |
| 9 | |
| 10 | const localserver = 'http://127.0.0.1:${port}' |
| 11 | |
| 12 | const exit_after = time.second * 10 |
| 13 | |
| 14 | pub struct App { |
| 15 | veb.StaticHandler |
| 16 | mut: |
| 17 | started chan bool |
| 18 | } |
| 19 | |
| 20 | pub fn (mut app App) before_accept_loop() { |
| 21 | app.started <- true |
| 22 | } |
| 23 | |
| 24 | @['/'] |
| 25 | pub fn (mut app App) index(mut ctx Context) veb.Result { |
| 26 | return ctx.text('Hello V!') |
| 27 | } |
| 28 | |
| 29 | @['/redirect_root'] |
| 30 | pub fn (mut app App) redirect_root(mut ctx Context) veb.Result { |
| 31 | return ctx.redirect('/root.txt') |
| 32 | } |
| 33 | |
| 34 | @[post] |
| 35 | pub fn (mut app App) post_request(mut ctx Context) veb.Result { |
| 36 | return ctx.text(ctx.req.data) |
| 37 | } |
| 38 | |
| 39 | pub struct Context { |
| 40 | veb.Context |
| 41 | } |
| 42 | |
| 43 | fn testsuite_begin() { |
| 44 | os.chdir(os.dir(@FILE))! |
| 45 | spawn fn () { |
| 46 | time.sleep(exit_after) |
| 47 | assert true == false, 'timeout reached!' |
| 48 | exit(1) |
| 49 | }() |
| 50 | |
| 51 | run_app_test() |
| 52 | } |
| 53 | |
| 54 | fn run_app_test() { |
| 55 | mut app := &App{} |
| 56 | if _ := app.handle_static('testdata', true) { |
| 57 | assert true == false, 'should throw unknown mime type error' |
| 58 | } else { |
| 59 | assert err.msg().starts_with('unknown MIME type for file extension ".what"'), 'throws error on unknown mime type' |
| 60 | } |
| 61 | |
| 62 | app.static_mime_types['.what'] = veb.mime_types['.txt'] |
| 63 | |
| 64 | if _ := app.handle_static('not_found', true) { |
| 65 | assert false, 'should throw directory not found error' |
| 66 | } else { |
| 67 | assert err.msg().starts_with('directory `not_found` does not exist') == true |
| 68 | } |
| 69 | |
| 70 | app.handle_static('testdata', true) or { panic(err) } |
| 71 | |
| 72 | // Enable markdown content negotiation for testing |
| 73 | app.enable_markdown_negotiation = true |
| 74 | |
| 75 | if _ := app.mount_static_folder_at('testdata', 'static') { |
| 76 | assert true == false, 'should throw invalid mount path error' |
| 77 | } else { |
| 78 | assert err.msg() == 'invalid mount path! The path should start with `/`' |
| 79 | } |
| 80 | |
| 81 | if _ := app.mount_static_folder_at('not_found', '/static') { |
| 82 | assert true == false, 'should throw mount path does not exist error' |
| 83 | } else { |
| 84 | assert err.msg().starts_with('directory `not_found` does not exist') == true |
| 85 | } |
| 86 | |
| 87 | app.mount_static_folder_at('testdata', '/static') or { panic(err) } |
| 88 | |
| 89 | spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) |
| 90 | // app startup time |
| 91 | _ := <-app.started |
| 92 | } |
| 93 | |
| 94 | fn test_static_root() { |
| 95 | x := http.get('${localserver}/root.txt')! |
| 96 | |
| 97 | assert x.status() == .ok |
| 98 | assert x.body == 'root' |
| 99 | } |
| 100 | |
| 101 | fn test_route_attribute_root_with_static_handler() { |
| 102 | x := http.get('${localserver}/')! |
| 103 | |
| 104 | assert x.status() == .ok |
| 105 | assert x.body == 'Hello V!' |
| 106 | } |
| 107 | |
| 108 | fn test_redirect_to_static_root() { |
| 109 | x := http.get('${localserver}/redirect_root')! |
| 110 | |
| 111 | assert x.status() == .ok |
| 112 | assert x.body == 'root' |
| 113 | } |
| 114 | |
| 115 | fn test_scans_subdirs() { |
| 116 | x := http.get('${localserver}/sub_folder/sub.txt')! |
| 117 | |
| 118 | assert x.status() == .ok |
| 119 | assert x.body == 'sub' |
| 120 | } |
| 121 | |
| 122 | fn test_index_subdirs() { |
| 123 | x := http.get('${localserver}/sub_folder/')! |
| 124 | y := http.get('${localserver}/sub.folder/sub_folder')! |
| 125 | |
| 126 | assert x.status() == .ok |
| 127 | assert x.body.trim_space() == 'OK' |
| 128 | |
| 129 | assert y.status() == .ok |
| 130 | assert y.body.trim_space() == 'OK' |
| 131 | } |
| 132 | |
| 133 | fn test_custom_mime_types() { |
| 134 | x := http.get('${localserver}/unknown_mime.what')! |
| 135 | |
| 136 | assert x.status() == .ok |
| 137 | assert x.header.get(.content_type)! == veb.mime_types['.txt'] |
| 138 | assert x.body.trim_space() == 'unknown_mime' |
| 139 | } |
| 140 | |
| 141 | fn test_custom_folder_mount() { |
| 142 | x := http.get('${localserver}/static/root.txt')! |
| 143 | |
| 144 | assert x.status() == .ok |
| 145 | assert x.body == 'root' |
| 146 | } |
| 147 | |
| 148 | fn test_upper_case_mime_type() { |
| 149 | x := http.get('${localserver}/upper_case.TXT')! |
| 150 | |
| 151 | assert x.status() == .ok |
| 152 | assert x.body == 'body' |
| 153 | } |
| 154 | |
| 155 | // Content negotiation tests - Priority order |
| 156 | // Tests verify: path.md > path.html.md > path/index.html.md |
| 157 | |
| 158 | fn test_markdown_negotiation_priority_first() { |
| 159 | // When all three variants exist, path.md (priority 1) is served |
| 160 | config := http.FetchConfig{ |
| 161 | url: '${localserver}/about' |
| 162 | header: http.new_header(key: .accept, value: 'text/markdown') |
| 163 | } |
| 164 | x := http.fetch(config)! |
| 165 | |
| 166 | assert x.status() == .ok |
| 167 | assert x.header.get(.content_type)! == 'text/markdown' |
| 168 | assert x.body.contains('This is the about page in markdown format.') |
| 169 | assert !x.body.contains('about.html.md variant') |
| 170 | assert !x.body.contains('about/index.html.md variant') |
| 171 | } |
| 172 | |
| 173 | fn test_markdown_negotiation_priority_second() { |
| 174 | // When only path.html.md exists (priority 2), it is served |
| 175 | config := http.FetchConfig{ |
| 176 | url: '${localserver}/page' |
| 177 | header: http.new_header(key: .accept, value: 'text/markdown') |
| 178 | } |
| 179 | x := http.fetch(config)! |
| 180 | |
| 181 | assert x.status() == .ok |
| 182 | assert x.header.get(.content_type)! == 'text/markdown' |
| 183 | assert x.body.contains('# Page HTML Markdown') |
| 184 | } |
| 185 | |
| 186 | fn test_markdown_negotiation_directory_index() { |
| 187 | // For directories, index.html.md is served when Accept: text/markdown |
| 188 | config := http.FetchConfig{ |
| 189 | url: '${localserver}/sub_folder/' |
| 190 | header: http.new_header(key: .accept, value: 'text/markdown') |
| 191 | } |
| 192 | x := http.fetch(config)! |
| 193 | |
| 194 | assert x.status() == .ok |
| 195 | assert x.header.get(.content_type)! == 'text/markdown' |
| 196 | assert x.body.contains('# Index HTML Markdown') |
| 197 | } |
| 198 | |
| 199 | // Direct access tests - Verifies backward compatibility |
| 200 | |
| 201 | fn test_markdown_direct_access() { |
| 202 | // Without Accept header |
| 203 | x_no_header := http.get('${localserver}/test.md')! |
| 204 | assert x_no_header.status() == .ok |
| 205 | assert x_no_header.header.get(.content_type)! == 'text/markdown' |
| 206 | assert x_no_header.body.contains('# Test Markdown') |
| 207 | |
| 208 | // With Accept: text/markdown header - same result |
| 209 | config := http.FetchConfig{ |
| 210 | url: '${localserver}/test.md' |
| 211 | header: http.new_header(key: .accept, value: 'text/markdown') |
| 212 | } |
| 213 | x_with_header := http.fetch(config)! |
| 214 | assert x_with_header.status() == .ok |
| 215 | assert x_with_header.header.get(.content_type)! == 'text/markdown' |
| 216 | assert x_with_header.body.contains('# Test Markdown') |
| 217 | } |
| 218 | |
| 219 | fn test_markdown_variants_direct_access() { |
| 220 | // All markdown variants remain accessible via their full paths |
| 221 | x_html_md := http.get('${localserver}/about.html.md')! |
| 222 | assert x_html_md.status() == .ok |
| 223 | assert x_html_md.body.contains('about.html.md variant') |
| 224 | |
| 225 | x_index := http.get('${localserver}/about/index.html.md')! |
| 226 | assert x_index.status() == .ok |
| 227 | assert x_index.body.contains('about/index.html.md variant') |
| 228 | } |
| 229 | |
| 230 | // Negative tests - Verifies correct behavior without Accept header |
| 231 | |
| 232 | fn test_markdown_no_negotiation_without_header() { |
| 233 | // Without Accept: text/markdown, content is not found for directories with no index.html |
| 234 | x := http.get('${localserver}/about')! |
| 235 | assert x.status() == .not_found |
| 236 | } |
| 237 | |