UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

678 lines (631 loc) 30.6 kB
######################################################################### # This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. # License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details ######################################################################### ### Test suite for PostgreSQL interface and functionality. COPYRIGHT : (c) 2017 SageMath, Inc. LICENSE : AGPLv3 ### # to run just this test, goto src/smc-hub/ and # SMC_DB_RESET=true SMC_TEST=true time node_modules/.bin/mocha --reporter ${REPORTER:-progress} test/postgres/postgres-test.coffee pgtest = require('./pgtest') db = undefined setup = (cb) -> (pgtest.setup (err) -> db=pgtest.db; cb(err)) teardown = pgtest.teardown {one_result, all_results, count_result} = require('../../postgres') async = require('async') expect = require('expect') misc = require('smc-util/misc') {DOMAIN_URL, SITE_NAME} = require('smc-util/theme') describe 'email verification: ', -> @timeout(5000) before(setup) after(teardown) locals = token : null email_address : "test@test.com" email_address2: "test2@test.com" data : null account_id : null it "creates a random token", (done) -> async.waterfall([ (cb) -> db.create_account(first_name:"A", last_name:"B", created_by:"1.2.3.4", email_address:locals.email_address, password_hash:"test", cb: cb) (account_id, cb) -> locals.account_id = account_id db.verify_email_create_token(account_id: account_id, cb:cb) ], (err, verify_info) -> {token} = verify_info expect(token.length > 5).toBe(true) # only lowercase, because upper/lower case in links in emails can get mangled for char in token expect(char in '0123456789abcdefghijklmnopqrstuvwxyz').toBe(true) locals.token = token done(err) ) it "correctly checks the token", (done) -> async.series([ (cb) -> db.verify_email_check_token(email_address:locals.email_address, token:locals.token, cb:cb) (cb) -> db._query query : "SELECT email_address_verified FROM accounts" where : "email_address = $::TEXT" : locals.email_address cb : one_result 'email_address_verified', (err, data) -> locals.data = data cb(err) (cb) -> # and that the token is deleted db._query query : 'SELECT email_address_challenge FROM accounts' where : "account_id = $::UUID" : locals.account_id cb : one_result (err, data) -> expect(Object.keys(data).length == 0).toBe(true) cb(err) ], (err) -> expect(locals.email_address in misc.keys(locals.data)).toBe(true) done(err) ) it "has no idea about unkown accounts", (done) -> db.verify_email_check_token(email_address:"other-one@test.com", token:locals.token, cb: (err) -> expect(!!err).toBe(true) expect(err.indexOf('no such account') != -1).toBe(true) done(undefined) # suppress error ) it "detects a wrong token", (done) -> async.waterfall([ (cb) -> db.create_account(first_name:"C", last_name:"D", created_by:"1.2.3.4", email_address:locals.email_address2, password_hash:"test", cb: cb) (account_id, cb) -> db.verify_email_create_token(account_id: account_id, cb:cb) (verify_info, cb) -> {token, email_address} = verify_info expect(email_address).toBe(locals.email_address2) db.verify_email_check_token email_address : locals.email_address2 token : "X" # a wrong one cb : (err) -> expect(err.indexOf('token does not match') != -1).toBe(true) cb(undefined) # suppress error ], done) it "returns the verified email address", (done) -> db.verify_email_get account_id : locals.account_id cb : (err, x) -> verified = x.email_address in misc.keys(x.email_address_verified) expect(verified).toBe(true) done(err) it "and also answers is_verified_email correctly", (done) -> async.series([ (cb) -> db.is_verified_email email_address : locals.email_address cb : (err, verified) -> expect(verified).toBe(true) cb(err) (cb) -> db.is_verified_email email_address : locals.email_address2 cb : (err, verified) -> expect(verified).toBe(false) cb(err) ], done) describe 'working with accounts: ', -> @timeout(5000) before(setup) after(teardown) it "checks that the account we haven't made yet doesn't already exist", (done) -> db.account_exists email_address: 'sage@example.com' cb:(err, exists) -> expect(!!exists).toBe(false); done(err) it "tries to get an account that doesn't exist", (done) -> db.get_account email_address:'sage@example.com' cb : (err, account) -> expect(err?).toBe(true); done() it "creates a new account", (done) -> db.create_account(first_name:"Sage", last_name:"Salvus", created_by:"1.2.3.4",\ email_address:"sage@example.com", password_hash:"blah", cb:done) it "checks the newly created account does exist", (done) -> db.account_exists email_address:'sage@example.com' cb:(err, exists) -> expect(!!exists).toBe(true); done(err) it "verifies that there is 1 account in the database via a count", (done) -> db.count table : 'accounts' cb : (err, n) -> expect(n).toBe(1); done(err) it "creates a second account", (done) -> db.create_account(first_name:"Mr", last_name:"Smith", created_by:"10.10.1.1",\ email_address:"sage-2@example.com", password_hash:"foo", cb:done) it "verifies that there are a total of 2 accounts in the database via the stats table", (done) -> db.get_stats(cb: (err, stats) -> expect(stats.accounts).toBe(2); done(err)) it "grabs our second new account by email and checks a name and property", (done) -> db.get_account email_address:'sage-2@example.com' cb:(err, account) -> expect(account.first_name).toBe("Mr") expect(account.password_is_set).toBe(true) done(err) it "computes number of accounts created from 1.2.3.4", (done) -> db.count_accounts_created_by ip_address : '1.2.3.4' age_s : 1000000 cb : (err, n) -> expect(n).toBe(1); done(err) it "deletes an account", (done) -> db.get_account email_address:'sage-2@example.com' cb : (err, account) -> db.delete_account account_id : account.account_id cb : done it "checks that account is gone", (done) -> db.account_exists email_address:'sage-2@example.com' cb:(err, exists) -> expect(!!exists).toBe(false); done(err) it "creates an account with no password set", (done) -> db.create_account(first_name:"Simple", last_name:"Sage", created_by:"1.2.3.4",\ email_address:"simple@example.com", cb:done) it "verifies that the password_is_set field is false", (done) -> db.get_account email_address:'simple@example.com' cb:(err, account) -> expect(account.password_is_set).toBe(false); done(err) describe 'working with logs: ', -> before(setup) after(teardown) it 'creates a log message', (done) -> db.log event : "test" value : "a message" cb : done it 'gets contents of the log and checks that the message we made is there', (done) -> db.get_log start : new Date(new Date() - 10000000) end : new Date() event : 'test' cb : (err, log) -> expect(log.length).toBe(1) expect(log[0]).toEqual(event:'test', value:'a message', id:log[0].id, time:log[0].time, expire:log[0].expire) done(err) it 'checks that there is nothing "old" in the log', (done) -> # no old stuff db.get_log start : new Date(new Date() - 10000000) end : new Date(new Date() - 1000000) cb : (err, log) -> expect(log.length).toBe(0); done(err) account_id = '4d29eec4-c126-4f06-b679-9a661fd7bcdf' error = "Your internet connection is unstable/down or #{SITE_NAME} is temporarily not available. Therefore #{SITE_NAME} is not working." event = 'test' it 'logs a client error', (done) -> db.log_client_error event : event error : error account_id : account_id cb : done it 'logs another client error with a different event', (done) -> db.log_client_error event : event + "-other" error : error account_id : account_id cb : done it 'gets the recent error log for only one event and checks that it has only one log entry in it', (done) -> db.get_client_error_log start : new Date(new Date() - 10000000) end : new Date() event : event cb : (err, log) -> expect(log.length).toBe(1) expect(log[0]).toEqual(event:event, error:error, account_id:account_id, id:log[0].id, time:log[0].time, expire:log[0].expire) done(err) it 'gets old log entries and makes sure there are none', (done) -> db.get_client_error_log start : new Date(new Date() - 10000000) end : new Date(new Date() - 1000000) event : event cb : (err, log) -> expect(log.length).toBe(0); done(err) describe 'testing working with blobs: ', -> @timeout(10000) beforeEach(setup) afterEach(teardown) {uuidsha1} = require('smc-util-node/misc_node') project_id = misc.uuid() it 'creating a blob and reading it', (done) -> blob = Buffer.from("This is a test blob") async.series([ (cb) -> db.save_blob(uuid : uuidsha1(blob), blob : blob, project_id : project_id, cb : cb) (cb) -> db.count table : 'blobs' cb : (err, n) -> expect(n).toBe(1) cb(err) (cb) -> db.get_blob uuid : uuidsha1(blob) cb : (err, blob2) -> expect(blob2.equals(blob)).toBe(true) cb(err) ], done) it 'tries to save a blob with an invalid uuid and gets an error', (done) -> db.save_blob uuid : 'not a uuid' blob : Buffer.from("This is a test blob") project_id : project_id cb : (err) -> expect(err).toEqual('uuid is invalid') done() it 'save a string blob (with a null byte!), and confirms it works (properly converted to Buffer)', (done) -> async.series([ (cb) -> db.save_blob(blob: 'my blob', project_id: project_id, cb: cb) (cb) -> db.get_blob uuid : uuidsha1('my blob') cb : (err, blob2) -> expect(blob2?.toString()).toEqual('my blob') cb(err) ], done) it 'creating 50 blobs and verifying that 50 are in the table', (done) -> async.series([ (cb) -> f = (n, cb) -> blob = Buffer.from("x#{n}") db.save_blob(uuid : uuidsha1(blob), blob : blob, project_id : project_id, cb : cb) async.map([0...50], f, cb) (cb) -> db.count table : 'blobs' cb : (err, n) -> expect(n).toBe(50) cb(err) ], done) it 'creating 5 blobs that expire in 0.01 second and 5 that do not, then wait 0.15s, delete_expired, then verify that the expired ones are gone from the table', (done) -> async.series([ (cb) -> f = (n, cb) -> blob = Buffer.from("x#{n}") db.save_blob(uuid : uuidsha1(blob), blob : blob, project_id : project_id, cb : cb, ttl:if n<5 then 0.01 else 0) async.map([0...10], f, cb) (cb) -> setTimeout(cb, 150) (cb) -> db.delete_expired(cb:cb) (cb) -> db.count table : 'blobs' cb : (err, n) -> expect(n).toBe(5) cb(err) ], done) it 'creating a blob that expires in 0.01 seconds, then extending it to never expire; wait, delete, and ensure it is still there', (done) -> blob = "a blob" uuid = uuidsha1(blob) async.series([ (cb) -> db.save_blob(uuid : uuid, blob : blob, project_id : project_id, cb : cb, ttl:0.01) (cb) -> db.remove_blob_ttls(uuids:[uuid], cb:cb) (cb) -> setTimeout(cb, 100) (cb) -> db.count table : 'blobs' cb : (err, n) -> expect(n).toBe(1) cb(err) ], done) describe 'testing the hub servers registration table: ', -> beforeEach(setup) afterEach(teardown) it 'test registering a hub that expires in 0.05 seconds, test is right, then wait 0.1s, delete_expired, then verify done', (done) -> async.series([ (cb) -> db.register_hub(host:"smc0", port:5000, clients:17, ttl:0.05, cb:cb) (cb) -> db.get_hub_servers cb:(err, v) -> expect(v.length).toBe(1) expect(v[0]).toEqual({host:"smc0-5000", port:5000, clients:17, expire:v[0].expire}) cb(err) (cb) -> setTimeout(cb, 150) (cb) -> db.delete_expired(cb:cb) (cb) -> db.get_hub_servers cb:(err, v) -> expect(v.length).toBe(0) cb(err) ], done) describe 'testing the server settings table: ', -> before(setup) after(teardown) it 'sets a server setting', (done) -> db.set_server_setting name : 'name' value : "some stuff" cb : done it 'reads that setting back', (done) -> db.get_server_setting name : 'name' cb : (err, value) -> expect(value).toEqual("some stuff") done(err) describe 'testing the passport settings table: ', -> before(setup) after(teardown) it 'creates a fake passport auth', (done) -> db.set_passport_settings(strategy:'fake', conf:{token:'foo'}, cb: done) it 'verifies that the fake passport was created', (done) -> db.get_passport_settings strategy : 'fake' cb : (err, value) -> expect(value).toEqual(token:'foo') done(err) it 'checks that it is also in the list of all passport entries', (done) -> db.get_all_passport_settings cb : (err, settings) -> if err done(err) else for s in settings if s.strategy == 'fake' and s.conf?.token == 'foo' done() return expect(false) # not found! describe 'user enumeration functionality: ', -> before(setup) after(teardown) num = 20 it "creates #{num} accounts", (done) -> f = (n, cb) -> db.create_account(first_name:"Sage#{n}", last_name:"Math#{n}", created_by:"1.2.3.4",\ email_address:"sage#{n}@sagemath.com", password_hash:"sage#{n}", cb:cb) async.map([0...num], f, done) it "searches for users using the 'sage' query", (done) -> db.user_search query : "sage" limit : num - 2 cb : (err, v) -> expect(v.length).toBe(num-2) done(err) it "searches for the user with email sage0@sagemath.com", (done) -> db.user_search query : "sage0@sagemath.com" cb : (err, users) -> expect(users.length).toBe(1) n = 0 data = users[0] delete data.created expect(data).toEqual( email_address: "sage0@sagemath.com", account_id:users[n].account_id, first_name: "Sage#{n}", last_name: "Math#{n}", email_address_verified: null, last_active: null ) done(err) it "searches for the non-existent user with email sageBLAH@sagemath.com", (done) -> db.user_search query : "sageBLAH@sagemath.com" cb : (err, users) -> expect(users.length).toBe(0); done(err) account_id = undefined it "adds another user", (done) -> db.create_account(first_name:"FOO", last_name:"BAR", created_by:"1.2.3.4",\ email_address:"foo@sagemath.com", password_hash:"sage", cb:(err, x) -> account_id=x; done(err)) it "then checks that the new user is found by first name", (done) -> db.user_search query : "FOO" cb : (err, users) -> expect(users.length).toBe(1); done(err) it "then checks that the new user is found by last name", (done) -> db.user_search query : "BAR" cb : (err, users) -> expect(users.length).toBe(1); done(err) it "change that user in place", (done) -> db._query query : "UPDATE accounts" set : {first_name:'VERT', last_name:'RAMP'} where : "account_id = $":account_id cb : done it "then checks that the modified user is found", (done) -> db.user_search query : "VERT" cb : (err, users) -> expect(users.length).toBe(1); done(err) it "but the previous name is not found", (done) -> db.user_search query : "BAR" cb : (err, users) -> expect(users.length).toBe(0); done(err) describe 'banning of users: ', -> before(setup) after(teardown) account_id = undefined it 'creates an account', (done) -> db.create_account(first_name:"Sage", last_name:"Math", created_by:"1.2.3.4",\ email_address:"sage@example.com", password_hash:"blah", cb:(err, x) => account_id=x; done(err)) it 'checks by account_id that the user we just created is not banned', (done) -> db.is_banned_user(account_id:account_id, cb:(err,x)=>expect(x).toBe(false); done(err)) it 'checks by email that user is not banned', (done) -> db.is_banned_user(email_address:"sage@example.com", cb:(err,x)=>expect(x).toBe(false); done(err)) it 'verifies that a user that does not exist is not banned', (done) -> db.is_banned_user(email_address:"sageXXX@example.com", cb:(err,x)=>expect(x).toBe(false); done(err)) it 'bans the user we created', (done) -> db.ban_user(account_id:account_id, cb:done) it 'checks they are banned by account_id', (done) -> db.is_banned_user(account_id:account_id, cb:(err,x)=>expect(x).toBe(true); done(err)) it 'checks they are banned by email address', (done) -> db.is_banned_user(email_address:"sage@example.com", cb:(err,x)=>expect(x).toBe(true); done(err)) it 'unbans our banned user', (done) -> db.unban_user(account_id:account_id, cb:done) it 'checks that the user we just unbanned is unbanned', (done) -> db.is_banned_user(account_id:account_id, cb:(err,x)=>expect(x).toBe(false); done(err)) it 'bans our user by email address instead', (done) -> db.ban_user(email_address:"sage@example.com", cb:done) it 'then checks that banning by email address worked', (done) -> db.is_banned_user(account_id:account_id, cb:(err,x)=>expect(x).toBe(true); done(err)) describe 'testing the passport table: ', -> before(setup) after(teardown) account_id = undefined it 'creates an account', (done) -> db.create_account(first_name:"Sage", last_name:"Math", created_by:"1.2.3.4",\ email_address:"sage@example.com", password_hash:"blah", cb:(err, x) => account_id=x; done(err)) it 'creates a passport', (done) -> db.create_passport account_id : account_id strategy : 'google' id : '929304823048' profile : {email_address:"sage@example.com", avatar:'James Cameron'} cb : done it 'checks the passport we just created exists', (done) -> db.passport_exists strategy : 'google' id : '929304823048' cb : (err, x) -> expect(x).toBe(account_id) done(err) it 'check that a non-existent passport does not exist', (done) -> db.passport_exists strategy : 'google' id : 'FAKE' cb : (err, x) -> expect(x).toBe(undefined) done(err) it 'check that a passport we created above exists directly via checking the accounts entry', (done) -> db.get_account account_id : account_id columns : ['passports'] cb : (err, x) -> expect(x.passports).toEqual( 'google-929304823048': { avatar: 'James Cameron', email_address: 'sage@example.com' }) done(err) it 'deletes the passport we made above', (done) -> db.delete_passport account_id : account_id strategy : 'google' id : '929304823048' cb : done it 'verifies the passport is really gone', (done) -> db.passport_exists strategy : 'google' id : '929304823048' cb : (err, x) -> expect(x).toBe(undefined) done(err) it 'check the passport is also gone from the accounts table', (done) -> db.get_account account_id : account_id columns : ['passports'] cb : (err, x) -> expect(misc.keys(x.passports).length).toEqual(0) done(err) it 'create two passports and verifies that both exist', (done) -> async.series([ (cb) -> db.create_passport account_id : account_id strategy : 'google' id : '929304823048' profile : {email_address:"sage@example.com", avatar:'James Cameron'} cb : cb (cb) -> db.create_passport account_id : account_id strategy : 'facebook' id : '12346' profile : {email_address:"sage@facebook.com", avatar:'Zuck'} cb : cb (cb) -> db.get_account account_id : account_id columns : ['passports'] cb : (err, x) -> expect(misc.keys(x.passports).length).toEqual(2) cb(err) ], done) describe 'testing file use notifications table: ', -> before(setup) after(teardown) account_id = undefined project_id = undefined path0 = "test_file" it 'creates an account', (done) -> db.create_account(first_name:"Sage", last_name:"Math", created_by:"1.2.3.4",\ email_address:"sage@example.com", password_hash:"blah", cb:(err, x) => account_id=x; done(err)) it 'creates a project', (done) -> db.create_project(account_id:account_id, title:"Test project", description:"The description",\ cb:(err, x) => project_id=x; done(err)) it "record editing of file '#{path0}'", (done) -> db.record_file_use(project_id: project_id, path:path0, account_id:account_id, action:"edit", cb:done) it "get activity for project and '#{path0}'", (done) -> db.get_file_use(project_id: project_id, path : path0, max_age_s : 1000, cb:(err, x)-> expect(x.project_id).toBe(project_id) expect(x.path).toBe(path0) expect(misc.keys(x.users)).toEqual([account_id]) expect(misc.keys(x.users[account_id])).toEqual(['edit']) done(err) ) it "get activity for the project and ensure there was is instance of activity", (done) -> db.get_file_use(project_id: project_id, max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(1); done(err)) path1 = "another_file" project_id1 = undefined it 'creates another project', (done) -> db.create_project(account_id:account_id, title:"Test project 2", description:"The description 2",\ cb:(err, x) => project_id1=x; done(err)) it "tests recording activity on another file '#{path1}'", (done) -> db.record_file_use(project_id: project_id1, path:path1, account_id:account_id, action:"edit", cb:done) it "gets activity only for the second project and checks there is only one entry", (done) -> db.get_file_use(project_id: project_id1, max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(1); done(err)) it "gets activity for both projects and checks there are two entries", (done) -> db.get_file_use(project_ids:[project_id, project_id1], max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(2); done(err)) it "gets all info about a project", (done) -> db.get_project project_id : project_id cb : (err, info) -> expect(info?.title).toEqual('Test project') expect(info?.project_id).toEqual(project_id) done(err) account_id1 = undefined path2 = "a_third_file" it 'creates another account', (done) -> db.create_account(first_name:"Sage1", last_name:"Math1", created_by:"1.2.3.4",\ email_address:"sage1@example.com", password_hash:"blah1", cb:(err, x) => account_id1=x; done(err)) it "records activity by new user on '#{path0}", (done) -> db.record_file_use(project_id: project_id, path:path0, account_id:account_id1, action:"edit", cb:done) it "checks that there is still one activity entry for first project", (done) -> db.get_file_use(project_id: project_id, max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(1); done(err)) it "checks two users are listed as editors on '#{path0}'", (done) -> db.get_file_use(project_id: project_id, path: path0, max_age_s : 1000, cb:(err, x)-> expect(misc.keys(x.users).length).toBe(2); done(err)) it "records activity by new user on '#{path2}", (done) -> db.record_file_use(project_id: project_id, path:path2, account_id:account_id1, action:"edit", cb:done) it "checks that there are two activity entries now for first project", (done) -> db.get_file_use(project_id: project_id, max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(2); done(err)) it "gets activity for both projects and checks there are now three entries", (done) -> db.get_file_use(project_ids:[project_id, project_id1], max_age_s : 1000, cb:(err, x)-> expect(x.length).toBe(3); done(err)) it "verifies that max_age_s filter works", (done) -> f = () -> db.get_file_use(project_ids:[project_id, project_id1], max_age_s:0.05, cb:(err, x)-> expect(x.length).toBe(0); done(err)) setTimeout(f,100) it "records edit action again on a file by a user and verifies that this changes the last_edited field", (done) -> last_edited = undefined async.series([ (cb) -> db.get_file_use(project_id:project_id, path: path0, max_age_s:1000, cb:(err, x)-> last_edited=x.last_edited; cb(err)) (cb) -> db.record_file_use(project_id:project_id, path:path0, account_id:account_id, action:"edit", cb:cb) (cb) -> db.get_file_use(project_id:project_id, path: path0, max_age_s:1000, cb:(err, x)-> expect(last_edited).toNotBe(x.last_edited); cb(err)) ], done) it "records seen action on a file by a user and verifies that this does not change the last_edited field and adds seen info", (done) -> async.series([ (cb) -> db.record_file_use(project_id:project_id, path:path0, account_id:account_id, action:"seen", cb:cb) (cb) -> db.get_file_use(project_id:project_id, path: path0, max_age_s:1000, cb:(err, x)-> expect(x.users[account_id].seen?).toBe(true) expect(x.users[account_id].read?).toBe(false) cb(err)) ], done) describe 'doing a "naked update"', -> it 'is an error', (done) -> db._query query : "UPDATE accounts SET first_name='William'" cb : (err) -> expect(err).toEqual("ERROR -- Dangerous UPDATE or DELETE without a WHERE, TRIGGER, or INSERT: query='UPDATE accounts SET first_name='William''") done()