//! The web backend: public repos over HTTP, and never a write. module remote import net import net.http struct FakeMirror { mut: asked []string } fn (mut h FakeMirror) handle(req http.Request) http.Response { h.asked << req.url if req.url == '/repo/koh/format' { return http.Response{ status_code: 200, status_msg: 'OK', body: 'koh-remote 1' } } if req.url == '/repo/koh/head' { return http.Response{ status_code: 200, status_msg: 'OK', body: 'abc123abc123' } } if req.url == '/repo/koh/saves/abc123abc123' { return http.Response{ status_code: 200, status_msg: 'OK', body: '{"id":"abc123abc123","files":[]}' } } if req.url == '/repo/koh/objects/dead' { // Raw bytes, not text: an object is zlib, and must survive intact. return http.Response{ status_code: 200, status_msg: 'OK', body: '\x01\x02\x03' } } if req.url == '/repo/koh/events' { return http.Response{ status_code: 200, status_msg: 'OK', body: 'e1\ne2\n' } } if req.url == '/repo/koh/broken' { return http.Response{ status_code: 500, status_msg: 'Internal Server Error' } } return http.Response{ status_code: 404, status_msg: 'Not Found' } } fn start_fake_mirror() !(int, &http.Server, &FakeMirror) { for port in 19321 .. 19331 { listener := net.listen_tcp(.ip, '127.0.0.1:${port}') or { continue } fake := &FakeMirror{} mut server := &http.Server{ listener: listener handler: fake show_startup_message: false } spawn server.listen_and_serve() server.wait_till_running()! return port, server, fake } return error('no free fake mirror port') } fn test_a_public_repo_reads_through_the_web_backend() { port, mut server, _ := start_fake_mirror()! defer { server.stop() } mut web := new_web('http://127.0.0.1:${port}')! format := web.get('repo/format')! assert format.found assert format.body.bytestr() == 'koh-remote 1' assert web.get('repo/head')!.body.bytestr() == 'abc123abc123' assert web.get('repo/saves/abc123abc123')!.body.bytestr() == '{"id":"abc123abc123","files":[]}' // Objects travel as stored, byte for byte: the client inflates them itself. blob := web.get('repo/objects/dead')! assert blob.found assert blob.body == [u8(1), 2, 3] // The event list becomes the keys `koh steal` expects. assert web.list('repo/events')! == ['repo/events/e1', 'repo/events/e2'] // A missing key is an answer, not a failure. assert !web.get('repo/objects/nope')!.found assert web.list('repo/events/none')! == []string{} } fn test_a_broken_mirror_and_every_write_are_refused() { port, mut server, _ := start_fake_mirror()! defer { server.stop() } mut web := new_web('http://127.0.0.1:${port}')! if _ := web.get('repo/broken') { assert false, 'a 500 must not read as a missing key' } else { assert err.msg().contains('500') } read_only := 'http://127.0.0.1:${port}/repo is read-only.' if _ := web.put_if_absent('repo/head', 'x'.bytes()) { assert false, 'a public repo is never written to' } else { assert err.msg() == read_only } if _ := web.put_if_match('repo/head', 'x'.bytes(), '"tag"') { assert false, 'a public repo is never written to' } else { assert err.msg() == read_only } if _ := web.delete('repo/head') { assert false, 'a public repo is never written to' } else { assert err.msg() == read_only } } fn test_only_a_bare_host_becomes_a_base() { assert kepr_host('kepr.uk')! == 'https://kepr.uk' assert kepr_host('https://kepr.uk/')! == 'https://kepr.uk' assert kepr_host('http://127.0.0.1:3917')! == 'http://127.0.0.1:3917' for bad in ['kepr.uk/foo', 'kepr.uk?x=1', 'user@kepr.uk', 'kepr uk', 'https://'] { if _ := kepr_host(bad) { assert false, '${bad} is not a host' } } } // The base is normalised once, so a call never builds a URL from what was typed. fn test_the_base_is_normalised_once() { port, mut server, fake := start_fake_mirror()! defer { server.stop() } mut web := new_web('http://127.0.0.1:${port}/')! assert web.base == 'http://127.0.0.1:${port}' assert web.get('repo/head')!.found assert fake.asked[0] == '/repo/koh/head' }