package router_tests import "core:fmt" import "core:os" import "core:strings" import "core:testing" import http "lib:net/http" import "lib:net/http/router" Trace :: struct { name: string, trace: ^[dynamic]string, } trace_middleware :: proc( req: router.Request, res: ^router.Response, ctx: rawptr, next: router.Route_Handler, next_ctx: rawptr, ) { t := (^Trace)(ctx) append(t.trace, t.name) next(req, res, next_ctx) } // Answers on its own, never calls next reject_middleware :: proc( req: router.Request, res: ^router.Response, ctx: rawptr, next: router.Route_Handler, next_ctx: rawptr, ) { t := (^Trace)(ctx) append(t.trace, t.name) http.respond(res, .Forbidden, "rejected") } trace_handler :: proc(req: router.Request, res: ^router.Response, ctx: rawptr) { t := (^Trace)(ctx) append(t.trace, t.name) http.respond(res, .Ok, "ok") } request :: proc(r: ^router.Router, target: string) -> http.Response { handler, data := router.handle(r) res: http.Response handler(http.Request{method = .Get, target = target}, &res, data) return res } response_destroy :: proc(res: ^http.Response) { delete(res.body) for h in res.headers { delete(h.name) delete(h.value) } delete(res.headers) } expect_trace :: proc(t: ^testing.T, trace: [dynamic]string, expected: []string, loc := #caller_location) { testing.expectf(t, len(trace) == len(expected), "trace %v, expected %v", trace[:], expected, loc = loc) if len(trace) == len(expected) { for e, i in expected { testing.expectf(t, trace[i] == e, "trace %v, expected %v", trace[:], expected, loc = loc) } } } @(test) middleware_order :: proc(t: ^testing.T) { trace: [dynamic]string defer delete(trace) g1 := Trace{"g1", &trace} g2 := Trace{"g2", &trace} r1 := Trace{"r1", &trace} r2 := Trace{"r2", &trace} h := Trace{"handler", &trace} r := router.create() defer router.destroy(&r) router.use(&r, trace_middleware, &g1) router.get(&r, "/x", trace_handler, user_data = &h, middlewares = {{trace_middleware, &r1}, {trace_middleware, &r2}}) // Added after the route, still global router.use(&r, trace_middleware, &g2) res := request(&r, "/x") defer response_destroy(&res) expect_trace(t, trace, {"g1", "g2", "r1", "r2", "handler"}) testing.expect_value(t, res.code, http.Status_Code.Ok) } @(test) route_middleware_only_runs_for_its_route :: proc(t: ^testing.T) { trace: [dynamic]string defer delete(trace) mw := Trace{"mw", &trace} h := Trace{"handler", &trace} r := router.create() defer router.destroy(&r) router.get(&r, "/guarded", trace_handler, user_data = &h, middlewares = {{trace_middleware, &mw}}) router.get(&r, "/open", trace_handler, user_data = &h) res := request(&r, "/open") response_destroy(&res) expect_trace(t, trace, {"handler"}) clear(&trace) res = request(&r, "/guarded") response_destroy(&res) expect_trace(t, trace, {"mw", "handler"}) } @(test) route_middleware_on_every_route_kind :: proc(t: ^testing.T) { Case :: struct { kind: router.Route_Kind, path: string, target: string, } cases := []Case { {.Exact, "/exact", "/exact"}, {.Prefix, "/prefix/", "/prefix/foo"}, {.Pattern, "/users/:id", "/users/42"}, } for c in cases { trace: [dynamic]string defer delete(trace) mw := Trace{"mw", &trace} h := Trace{"handler", &trace} r := router.create() defer router.destroy(&r) router.get(&r, c.path, trace_handler, c.kind, &h, {{trace_middleware, &mw}}) res := request(&r, c.target) defer response_destroy(&res) expect_trace(t, trace, {"mw", "handler"}) } } // Used to leak the nodes of every middleware below the one that answered @(test) middleware_can_short_circuit :: proc(t: ^testing.T) { trace: [dynamic]string defer delete(trace) g1 := Trace{"g1", &trace} g2 := Trace{"g2", &trace} deny := Trace{"deny", &trace} r1 := Trace{"r1", &trace} h := Trace{"handler", &trace} r := router.create() defer router.destroy(&r) router.use(&r, trace_middleware, &g1) router.use(&r, reject_middleware, &deny) router.use(&r, trace_middleware, &g2) router.get(&r, "/x", trace_handler, user_data = &h, middlewares = {{trace_middleware, &r1}}) res := request(&r, "/x") defer response_destroy(&res) expect_trace(t, trace, {"g1", "deny"}) testing.expect_value(t, res.code, http.Status_Code.Forbidden) } // More middlewares than fit in the on-stack chain storage @(test) long_chain :: proc(t: ^testing.T) { COUNT :: 12 trace: [dynamic]string defer delete(trace) names: [COUNT]string traces: [COUNT]Trace expected: [COUNT + 1]string h := Trace{"handler", &trace} r := router.create() defer router.destroy(&r) for i in 0 ..< COUNT { names[i] = fmt.aprintf("m%d", i) traces[i] = Trace{names[i], &trace} expected[i] = names[i] router.use(&r, trace_middleware, &traces[i]) } defer for n in names { delete(n) } expected[COUNT] = "handler" router.get(&r, "/x", trace_handler, user_data = &h) res := request(&r, "/x") response_destroy(&res) expect_trace(t, trace, expected[:]) // ...and bailing out at the end of it, the handler is never reached clear(&trace) deny := Trace{"deny", &trace} router.use(&r, reject_middleware, &deny) res = request(&r, "/x") defer response_destroy(&res) expected[COUNT] = "deny" expect_trace(t, trace, expected[:]) testing.expect_value(t, res.code, http.Status_Code.Forbidden) } @(test) static_sees_mime_types_added_later :: proc(t: ^testing.T) { dir, err := os.make_directory_temp("", "gloam-router-test-*", context.allocator) if !testing.expectf(t, err == nil, "couldn't create temp dir: %v", err) { return } defer delete(dir) defer os.remove_all(dir) file, _ := os.join_path({dir, "a.wasm"}, context.allocator) defer delete(file) testing.expect(t, os.write_entire_file(file, "contents") == nil) r := router.create() defer router.destroy(&r) router.static(&r, "/static", dir) // Enough to make the map reallocate extra: [64]string for i in 0 ..< len(extra) { extra[i] = fmt.aprintf(".ext%d", i) r.mime_map[extra[i]] = "x/y" } defer for e in extra { delete(e) } r.mime_map[".wasm"] = "application/wasm" res := request(&r, "/static/a.wasm") defer response_destroy(&res) testing.expect_value(t, res.code, http.Status_Code.Ok) testing.expect_value(t, string(res.body), "contents") ct, _ := http.headers_get_first(res.headers[:], "content-type") testing.expect_value(t, ct, "application/wasm") } // Answers with the value of the param named by its user_data Param_Probe :: struct { name: string, } param_handler :: proc(req: router.Request, res: ^router.Response, ctx: rawptr) { probe := (^Param_Probe)(ctx) http.respond(res, .Ok, req.params[probe.name]) } expect_response :: proc( t: ^testing.T, r: ^router.Router, target: string, code: http.Status_Code, body := "", loc := #caller_location, ) { res := request(r, target) defer response_destroy(&res) testing.expectf(t, res.code == code, "%s: code %v, expected %v", target, res.code, code, loc = loc) if code == .Ok { testing.expectf(t, string(res.body) == body, "%s: body %q, expected %q", target, string(res.body), body, loc = loc) } } @(test) pattern_needs_exact_segment_count :: proc(t: ^testing.T) { probe := Param_Probe{"b"} r := router.create() defer router.destroy(&r) router.get(&r, "/a/:b", param_handler, .Pattern, &probe) expect_response(t, &r, "/a/x", .Ok, "x") expect_response(t, &r, "/a/x/y", .NotFound) expect_response(t, &r, "/a", .NotFound) expect_response(t, &r, "/a/", .NotFound) // empty param } @(test) pattern_wildcard_takes_the_rest :: proc(t: ^testing.T) { files := Param_Probe{"path"} r := router.create() defer router.destroy(&r) router.get(&r, "/files/*path", param_handler, .Pattern, &files) expect_response(t, &r, "/files/x", .Ok, "x") expect_response(t, &r, "/files/a/b/c", .Ok, "a/b/c") expect_response(t, &r, "/files/", .Ok, "") expect_response(t, &r, "/files", .Ok, "") // matches nothing at all expect_response(t, &r, "/filesx", .NotFound) expect_response(t, &r, "/other/a", .NotFound) // A wildcard as the very first segment r2 := router.create() defer router.destroy(&r2) router.get(&r2, "/*path", param_handler, .Pattern, &files) expect_response(t, &r2, "/a/b", .Ok, "a/b") expect_response(t, &r2, "/a", .Ok, "a") expect_response(t, &r2, "/", .Ok, "") } // The route table of a repository browser, links like /r/repo/tree/ref have to keep working @(test) repo_browser_routes :: proc(t: ^testing.T) { Probe :: struct { name: string, } probe :: proc(req: router.Request, res: ^router.Response, ctx: rawptr) { http.respond(res, .Ok, fmt.tprintf("%s %s|%s|%s", (^Probe)(ctx).name, req.params["repo"], req.params["ref"], req.params["path"])) } raw, blob, tree := Probe{"raw"}, Probe{"blob"}, Probe{"tree"} r := router.create() defer router.destroy(&r) router.get(&r, "/r/:repo/raw/:ref/*path", probe, .Pattern, &raw) router.get(&r, "/r/:repo/blob/:ref/*path", probe, .Pattern, &blob) router.get(&r, "/r/:repo", probe, .Pattern, &tree) router.get(&r, "/r/:repo/tree/:ref/*path", probe, .Pattern, &tree) expect_response(t, &r, "/r/glaze", .Ok, "tree glaze||") expect_response(t, &r, "/r/glaze/tree/main", .Ok, "tree glaze|main|") expect_response(t, &r, "/r/glaze/tree/1a2b3c4", .Ok, "tree glaze|1a2b3c4|") expect_response(t, &r, "/r/glaze/tree/main/", .Ok, "tree glaze|main|") expect_response(t, &r, "/r/glaze/tree/main/src/dir", .Ok, "tree glaze|main|src/dir") expect_response(t, &r, "/r/glaze/blob/main/src/main.odin", .Ok, "blob glaze|main|src/main.odin") expect_response(t, &r, "/r/glaze/raw/dev/README.md", .Ok, "raw glaze|dev|README.md") expect_response(t, &r, "/r/glaze/blob/main", .Ok, "blob glaze|main|") expect_response(t, &r, "/r/glaze/nope/main/x", .NotFound) } @(test) pattern_with_empty_segments :: proc(t: ^testing.T) { probe := Param_Probe{"x"} r := router.create() defer router.destroy(&r) router.get(&r, "/", param_handler, .Pattern, &probe) router.get(&r, "/a//b", param_handler, .Pattern, &probe) expect_response(t, &r, "/", .Ok, "") expect_response(t, &r, "/a//b", .Ok, "") expect_response(t, &r, "/a/b", .NotFound) } @(test) pattern_wildcard_must_be_last :: proc(t: ^testing.T) { probe := Param_Probe{"rest"} r := router.create() defer router.destroy(&r) router.get(&r, "/a/*rest/b", param_handler, .Pattern, &probe) // refused router.get(&r, "/p/:", param_handler, .Pattern, &probe) // nameless param, refused router.get(&r, "no-slash/:x", param_handler, .Pattern, &probe) // refused expect_response(t, &r, "/a/x/b", .NotFound) expect_response(t, &r, "/p/x", .NotFound) expect_response(t, &r, "no-slash/x", .NotFound) } @(test) odd_targets_dont_panic :: proc(t: ^testing.T) { probe := Param_Probe{"x"} r := router.create() defer router.destroy(&r) router.get(&r, "/:x", param_handler, .Pattern, &probe) expect_response(t, &r, "", .NotFound) expect_response(t, &r, "*", .NotFound) expect_response(t, &r, "http://host/x", .NotFound) } @(private = "file") Static_Fixture :: struct { dir: string, // holds www/ and www-secret/ root: string, // dir/www } @(private = "file") static_fixture :: proc(t: ^testing.T) -> (f: Static_Fixture, ok: bool) { err: os.Error f.dir, err = os.make_directory_temp("", "gloam-router-test-*", context.allocator) if !testing.expectf(t, err == nil, "couldn't create temp dir: %v", err) { return } f.root, _ = os.join_path({f.dir, "www"}, context.allocator) secret_dir, _ := os.join_path({f.dir, "www-secret"}, context.allocator) defer delete(secret_dir) testing.expect(t, os.make_directory(f.root) == nil) testing.expect(t, os.make_directory(secret_dir) == nil) write :: proc(t: ^testing.T, elems: []string, contents: string) { path, _ := os.join_path(elems, context.allocator) defer delete(path) testing.expect(t, os.write_entire_file(path, contents) == nil) } write(t, {f.root, "a.txt"}, "public") write(t, {f.dir, "www-secret", "b.txt"}, "secret") write(t, {f.dir, "outside.txt"}, "outside") return f, true } @(private = "file") static_fixture_destroy :: proc(f: Static_Fixture) { os.remove_all(f.dir) delete(f.root) delete(f.dir) } @(test) static_stays_inside_root :: proc(t: ^testing.T) { f, ok := static_fixture(t) if !ok { return } defer static_fixture_destroy(f) r := router.create() defer router.destroy(&r) router.static(&r, "/static", f.root) expect_response(t, &r, "/static/a.txt", .Ok, "public") expect_response(t, &r, "/static/../outside.txt", .NotFound) // Shares the leading characters of the root, but is a sibling expect_response(t, &r, "/static/../www-secret/b.txt", .NotFound) expect_response(t, &r, "/static/a.txt\x00/../../outside.txt", .NotFound) } @(test) static_with_unresolvable_root_serves_nothing :: proc(t: ^testing.T) { f, ok := static_fixture(t) if !ok { return } defer static_fixture_destroy(f) missing, _ := os.join_path({f.dir, "does-not-exist"}, context.allocator) defer delete(missing) outside, _ := os.join_path({f.dir, "outside.txt"}, context.allocator) defer delete(outside) r := router.create() defer router.destroy(&r) router.static(&r, "/static", missing) // An absolute path as the target used to get through an empty root target := strings.concatenate({"/static/", outside}) defer delete(target) expect_response(t, &r, target, .NotFound) }