class Service(object, metaclass=Singleton): def __init__(self): self.repository = Repository() @log def save(self, body): entity = json.loads(body, cls=EntityDecoder) return json.dumps(self.repository.save(entity=entity), cls=EntityEncoder) @log def update(self, body): entity = json.loads(body, cls=EntityDecoder) return json.dumps(self.repository.update(entity=entity), cls=EntityEncoder) @log def remove(self, body): entity = json.loads(body, cls=EntityDecoder) return json.dumps(self.repository.remove(entity=entity)) @log def remove_by_id(self, body): return json.dumps( self.repository.remove_by_id(entity_id=json.loads(body)['id'])) @log def get(self, entity_id): return json.dumps(self.repository.get(entity_id=entity_id), cls=EntityEncoder)
class Serice(object, metaclass=Singleton): def __init__(self): self.repository = Repository() @log def get_all(self): entities = [Entity(int(x[0]), x[1]) for x in self.repository.get_all()] return json.dumps(entities, cls=EntityJsonEncoder) @log def get_by_id(self, id): r = self.repository.get_by_id(id) entity = r and Entity(int(r[0]), r[1]) or Entity() return json.dumps(entity, cls=EntityJsonEncoder) @log def save(self, entity): return json.dumps(self.repository.save( json.loads(entity, cls=EntityJsonDecoder)), cls=EntityJsonEncoder) @log def delete_by_id(self, id): return json.dumps(self.repository.delete_by_id(id)) @log def update(self, entity): return json.dumps(self.repository.update( json.loads(entity, cls=EntityJsonDecoder)), cls=EntityJsonEncoder)
def test_receive_obs_error(self): """Test error handling in the receive routine.""" repository = Repository() station_id = 1 # Check what happens if the path is misconfigured (or the server is not able to write file) self.app.root = app.config["storage"][ 'image_root'] = '/nonexistent/path' secret = repository.read_station_secret(station_id) data = { 'aos': datetime.datetime(2020, 3, 28, 12, 00), 'tca': datetime.datetime(2020, 3, 28, 12, 15), 'los': datetime.datetime(2020, 3, 28, 12, 30), 'sat': 'NOAA 15', # 'notes': optional, "file0": open("tests/x.png", 'rb'), "file1": open("tests/x.png", 'rb') } header_value = get_authorization_header_value(str(station_id), secret, data) headers = {'Authorization': header_value} response = self.app.post('/receive', data=data, headers=headers) self.assertEqual(response.status_code, 503) # Check if there's appropriate entry in the log file. self.check_log([ "Failed to write /nonexistent/path/", "tests_x.png (image_root=/nonexistent/path)" ])
def _get_secret(station_id) -> bytes: ''' Fetch station secret from database ToDo: Returned value should be cached to avoid DDoS and DB call before authorization ''' repository = Repository() return repository.read_station_secret(station_id)
def test_read_observation(self, repository: Repository): obs = repository.read_observation(ObservationId(750)) self.assertIsNotNone(obs) # Now check if the response is as expected. self.check_obs750(obs) # type: ignore # Now test negative case. There's nos uch observation obs = repository.read_observation(ObservationId(12345)) assert obs == None
def login(): repository = Repository() if current_user.is_authenticated: stations = repository.owned_stations(current_user.get_id()) # list of stations l = " ".join(f"{s['name']}({s['station_id']})" for s in stations) app.logger.info("Authenticated user %s, owner of %s" % (current_user.username, l)) return render_template("login.html", user=current_user, stations=stations) form = LoginForm() if form.validate_on_submit(): app.logger.info( "Login requested for user %s, pass=%s, remember_me=%s" % (form.username.data, form.password.data, form.remember.data)) user = repository.read_user(user=form.username.data) if user is None: app.logger.info("Login failed: invalid username: %s" % form.username.data) flash("Invalid username.") return redirect(url_for("login")) u = ApplicationUser(user) if not u.check_password(form.password.data): app.logger.info("Login failed: invalid password %s for user %s" % (form.password.data, form.username.data)) flash("Invalid password.") return redirect(url_for("login")) if u.role == UserRole.BANNED: app.logger.info( "Login failed: attempt to login into disabled account %s" % form.username.data) flash("Account disabled.") return redirect(url_for("login")) app.logger.info("Login successful for user %s" % form.username.data) login_user(u, remember=form.remember.data) next_page = request.args.get("next") if not next_page or url_parse(next_page).netloc != "": next_page = url_for("index") return redirect(next_page) return render_template("login.html", form=form)
def test_db_version(self, mock_connect): repostory = Repository({}) mock_cursor = mock_connect.return_value.cursor.return_value mock_cursor.fetchone.side_effect = [{ "count": 1 }, (3, ), { "version": 15 }] version = repostory.get_database_version() self.assertEqual(version, 15)
def test_user_role(self, repository: Repository): """Tests conversion of string to user roles.""" self.assertEqual(repository.user_role_to_enum('REGULAR'), UserRole.REGULAR) self.assertEqual(repository.user_role_to_enum('OWNER'), UserRole.OWNER) self.assertEqual(repository.user_role_to_enum('ADMIN'), UserRole.ADMIN) self.assertEqual(repository.user_role_to_enum('BANNED'), UserRole.BANNED) self.assertRaises(LookupError, repository.user_role_to_enum, 'moderator') # no such role
def test_station(self, repository: Repository): """Check that a single station data is returned properly.""" station = repository.read_station(1) statistics = repository.read_station_statistics(1) self.assertIsNotNone(station) self.assertIsNotNone(statistics) self.check_station1(station, statistics) # Now check invalid case. There's no such station station = repository.read_station(123) statistics = repository.read_station_statistics(123) self.assertIsNone(station) self.assertIsNone(statistics)
def test_receive_obs(self): repository = Repository() station_id = 1 secret = repository.read_station_secret(station_id) data = { 'aos': datetime.datetime(2020, 3, 28, 12, 00), 'tca': datetime.datetime(2020, 3, 28, 12, 15), 'los': datetime.datetime(2020, 3, 28, 12, 30), 'sat': 'NOAA 15', 'config': '{"text":"note text"}', "file0": open("tests/x.png", 'rb'), "file1": open("tests/x.png", 'rb'), "tle": [ # Include trailling character "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927 ", "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537" ], "rating": 0.75 } header_value = get_authorization_header_value(str(station_id), secret, data) headers = {'Authorization': header_value} response = self.app.post('/receive', data=data, headers=headers) self.assertEqual(response.status_code, 204) file_count = lambda dir_: len([ f for f in os.listdir(dir_) if os.path.isfile(os.path.join(dir_, f)) ]) self.assertEqual(file_count(IMAGE_ROOT), 2) self.assertEqual(file_count(os.path.join(IMAGE_ROOT, "thumbs")), 1) chart_dir = os.path.join(IMAGE_ROOT, "charts") self.assertEqual(file_count(chart_dir), 2) chart_files = sorted(os.listdir(chart_dir)) self.assertEqual(chart_files, ["by_time-1.png", "polar-1.png"]) # Todo: Need to check if the DB entries have been added. # Check if there are appropriate entries in the log file. self.check_log([ "0-tests_x.png written to tests/images", "1-tests_x.png written to tests/images" ])
def test_satellites(self, repository: Repository): """Test that a list of satellites is returned properly.""" satellites = repository.read_satellites() self.assertEqual(len(satellites), 3) self.assertEqual(satellites[-1]['sat_id'], 33591) self.assertEqual(satellites[-2]['sat_id'], 28654) self.assertEqual(satellites[-3]['sat_id'], 25338) satellites = repository.read_satellites(limit=2) self.assertEqual(len(satellites), 2) self.assertEqual(satellites[-1]['sat_id'], 28654) self.assertEqual(satellites[-2]['sat_id'], 25338) satellites = repository.read_satellites(offset=2) self.assertEqual(len(satellites), 1) self.assertEqual(satellites[-1]['sat_id'], 33591)
def create_repo(): request_data = request.get_json() new_repo = None repo_id = None repo_url = None expires = None try: new_repo = Repository.from_request(request_data) except AttributeError as e: abort(400, 'Bad request. %s' % e.__cause__) try: if new_repo is not None: (repo_id, repo_url) = new_repo.init() builder = BuildPipeline(new_repo) builder.execute() expires = RepoUtils.get_expiration_date(repo_id) except RepositoryError as e: current_app.logger.error(e.__str__()) deploy_error_page(new_repo.deploy_path) except OSError as e: current_app.logger.error(e.__str__()) deploy_error_page(new_repo.deploy_path) finally: if repo_url is not None and repo_id is not None and expires is not None: return jsonify({ Constants.RESPONSE_PARAMS['PREVIEW_URL']: repo_url, Constants.RESPONSE_PARAMS['PREVIEW_EXPIRATION']: expires, Constants.RESPONSE_PARAMS['PREVIEW_ID']: repo_id }) else: abort(502)
def test_owned_stations(self, repository: Repository): """Tests that it's possible to get a list of stations a given user is allowed to manage.""" stations = repository.owned_stations( 5) # get the list of stations that user with user_id=5 owns. self.assertEqual(len(stations), 1) self.assertEqual(stations[0]['name'], 'TKiS-1') self.assertEqual(stations[0]['station_id'], 1)
def test_satellite(self, repository: Repository): """Checks that a single satellite data is returned properly.""" sat = repository.read_satellite(25338) self.assertIsNotNone(sat) self.assertEqual(sat['sat_id'], 25338) self.assertEqual(sat['sat_name'], 'NOAA 15') sat = repository.read_satellite('NOAA 19') self.assertEqual(sat['sat_id'], 33591) self.assertEqual(sat['sat_name'], 'NOAA 19') sat = repository.read_satellite(12345) self.assertIsNone(sat) # :( No such thing yet. sat = repository.read_satellite('GDANSKSAT-1') self.assertIsNone(sat) # :( No such thing yet.
def test_is_station_owner(self, repository: Repository): """Tests if the ownership relation can be checked properly. The input data is defined in the db-data.psql file. Here it's copied for convenience: (1,1), (2,1), (3,1), (5,1), (4,2)""" cases = [[1, 1, True], [2, 2, False], [2, 1, True], [3, 1, True], [4, 1, False], [5, 1, True], [4, 2, True]] for c in cases: # Checking if user {c[0]} owns the station {c[1]}, expected result {c[2]} self.assertEqual(repository.is_station_owner(c[0], c[1]), c[2])
def obs(obs_id: ObservationId = None, limit_and_offset=None): if obs_id is None: abort(300, description="ID is required") return repository = Repository() with repository.transaction(): observation = repository.read_observation(obs_id) orbit = None if observation is None: abort(404, "Observation not found") files = repository.read_observation_files(observation["obs_id"], **limit_and_offset) files_count = repository.count_observation_files(obs_id) satellite = repository.read_satellite(observation["sat_id"]) orbit = observation if observation['tle'] is not None: # observation['tle'] is always an array of exactly 2 strings. orbit = parse_tle(*observation['tle'], satellite["sat_name"]) station = repository.read_station(observation["station_id"]) # Now tweak some observation parameters to make them more human readable observation = human_readable_obs(observation) # Now determine if there is a logged user and if there is, if this user is the owner of this # station. If he is, we should show the admin panel. user_id = 0 owner = False if current_user.is_authenticated: user_id = current_user.get_id() # Check if the current user is the owner of the station. station_id = station['station_id'] owner = repository.is_station_owner(user_id, station_id) return 'obs.html', dict(obs=observation, files=files, sat_name=satellite["sat_name"], item_count=files_count, orbit=orbit, station=station, is_owner=owner)
def station(station_id=None): repository = Repository() station = repository.read_station(station_id) if station is None: abort(404, "Station not found") statistics = repository.read_station_statistics(station["station_id"]) photos = repository.read_station_photos(station_id) owners = repository.station_owners(station_id) # Now get 3 latest observations from this station filters = {"station_id": station['station_id']} latest_obs = repository.read_observations(filters=filters, limit=3, offset=0) # Get the 3 observations with the best rating best_obs = repository.read_observations(filters=filters, limit=3, offset=0, order="r.rating DESC", expr="r.rating is not NULL") x = {} x['station_id'] = station['station_id'] x['name'] = station['name'] x['coords'] = utils.coords(station['lon'], station['lat']) x['descr'] = station['descr'] x['config'] = station['config'] x['registered'] = station['registered'] x['lastobs'] = statistics["last_los"] x['firstobs'] = statistics["first_aos"] x['cnt'] = statistics["observation_count"] files = [] for photo in photos: y = {} y['filename'] = photo['filename'] y['descr'] = photo['descr'] y['sort'] = photo['sort'] files.append(y) return render_template('station.html', station=x, files=files, owners=owners, latest_obs=latest_obs, best_obs=best_obs)
def test_read_observations(self, repository: Repository): """Check if a list of observations is returned properly.""" obslist = repository.read_observations() # Make sure there are at least 3 observations (more may be added in the future. # This test should be future-proof.) self.assertGreaterEqual(len(obslist), 3) # Check if the data returned matches values from tests/db-data.psql self.check_obs750(obslist[-1]) self.check_obs751(obslist[-2]) self.check_obs752(obslist[-3])
def test_station_owners(self, repository: Repository): """Tests that the owners of a station can be returned properly. This query is used on the station page to list its owners.""" owners1 = repository.station_owners(1) # List owners of station id 1 owners5 = repository.station_owners( 5 ) # List owners of station id 5 (no such station, so should be empty list) # See tests/db-data.psql for details (station_owners table) self.assertEqual(len(owners1), 4) self.assertEqual(owners1[0]['username'], 'asimov') self.assertEqual(owners1[0]['id'], 1) self.assertEqual(owners1[1]['username'], 'baxter') self.assertEqual(owners1[1]['id'], 2) self.assertEqual(owners1[2]['username'], 'clarke') self.assertEqual(owners1[2]['id'], 3) self.assertEqual(owners1[3]['username'], 'admin') self.assertEqual(owners1[3]['id'], 5) # No such station, so no owners self.assertEqual(len(owners5), 0)
def test_stations(self, repository: Repository): """Checks that a list of stations is returned properly.""" station_entries = repository.read_stations() station_statistics = repository.read_stations_statistics() self.assertEqual(len(station_entries), 2) self.assertEqual(len(station_statistics), 2) s1, s2 = zip(station_entries, station_statistics) self.check_station1(*s1) self.check_station2(*s2) # Now limit number of returned stations to just one. There should be only station-id 1. station_entries = repository.read_stations(limit=1) station_statistics = repository.read_stations_statistics(limit=1) self.assertEqual(len(station_entries), 1) self.assertEqual(len(station_statistics), 1) self.check_station1(station_entries[0], station_statistics[0] ) # This should return values for station-id 1 # Now skip the first station. Only station 2 should be returned. station_entries = repository.read_stations(offset=1) station_statistics = repository.read_stations_statistics(offset=1) self.assertEqual(len(station_entries), 1) self.assertEqual(len(station_statistics), 1) self.check_station2(station_entries[0], station_statistics[0] ) # This should return values for station-id 2
class Service(object, metaclass=Singleton): def __init__(self): self.repository = Repository() @log() @to_json(clazz=Entity) def save(self, body): return self.repository.save(body) @log() @to_json(clazz=Entity) def get(self, id): return self.repository.get(id) @log() def remove(self, id): return self.repository.remove(id) @log() @to_json(clazz=Entity) def update(self, body): return self.repository.update(body)
def obs_delete(obs_id: ObservationId = None): # First check if such an observation even exists. repository = Repository() observation = repository.read_observation(obs_id) if observation is None: return render_template('obs_delete.html', status=["There is no observation %s" % obs_id], obs_id=obs_id) # Second, check if the guy is logged in. if not current_user.is_authenticated: return render_template( 'obs_delete.html', status=["You are not logged in, you can't delete anything."], obs_id=obs_id) # Ok, at least this guy is logged in. Let's check who he is. user_id = current_user.get_id() # Check if the current user is the owner of the station. station = repository.read_station(observation["station_id"]) station_id = station['station_id'] owner = repository.is_station_owner(user_id, station_id) if not owner: return render_template( 'obs_delete.html', status=[ "You are not the owner of station %s, you can't delete observation %s." % (station.name, obs_id) ], obs_id=obs_id) # If you got that far, this means the guy is logged in, he's the owner and is deleting his own observation. status = obs_delete_db_and_disk(repository, obs_id) return render_template('obs_delete.html', status=status, obs_id=obs_id)
def obs_delete_db_and_disk(repository: Repository, obs_id: ObservationId): app.logger.info("About to delete observation %s and all its files" % obs_id) # Step 1: Create a list of files to be deleted. There may be several products. products = repository.read_observation_files(obs_id) obs = repository.read_observation(obs_id) files = [[f['filename'], "product"] for f in products] # Step 2: thumbnail is stored with the observation. There's at most one thumbnail. files.append([os.path.join("thumbs", obs['thumbnail']), "thumbnail"]) # Step 3: And there are two charts: alt-az pass chart and polar pass chart. files.append( [os.path.join("charts", "by_time-%s.png" % obs_id), "pass chart"]) files.append( [os.path.join("charts", "polar-%s.png" % obs_id), "polar pass chart"]) # All those files are stored in this dir root = app.config["storage"]['image_root'] status = [] for f in files: path = os.path.join(root, f[0]) app.logger.info("Trying to delete [%s]" % path) try: os.remove(path) status.append("Deleted %s file %s." % (f[1], f[0])) except Exception as ex: status.append("Failed to delete %s file: %s, reason: %s" % (f[1], path, repr(ex))) # Step 4: delete entries in the db repository.delete_observation(obs_id) status.append("DB removal complete.") return status
class Service(metaclass=Singleton): def __init__(self): self.repository = Repository() @log def save(self, body): entity = json.loads(body, cls=EntityDecoder) return json.dumps(self.repository.create(entity), cls=EntityEncoder) @log def remove(self, id): return json.dumps(self.repository.remove(id), cls=EntityEncoder) @log def get(self, id): return json.dumps(self.repository.get(id), cls=EntityEncoder) @log def update(self, body): entity = json.loads(body, cls=EntityDecoder) return json.dumps(self.repository.update(entity), cls=EntityEncoder)
class AppHandler(tornado.web.RequestHandler): def initialize(self): self.repository = Repository() def get(self): self.write( json.dumps(self.repository.get(self.get_argument('id')), cls=EntityEncoder)) def post(self): self.write( json.dumps(self.repository.update( json.loads(self.request.body, cls=EntityDecoder)), cls=EntityEncoder)) def put(self): self.write( json.dumps(self.repository.create( json.loads(self.request.body, cls=EntityDecoder)), cls=EntityEncoder)) def delete(self): self.write(json.dumps(self.repository.remove(self.get_argument('id'))))
def stations(limit_and_offset): '''This function retrieves list of all registered ground stations.''' repository = Repository() stations = repository.read_stations(**limit_and_offset) statistics = repository.read_stations_statistics(**limit_and_offset) station_count = repository.count_stations() # Now convert the data to a list of objects that we can pass to the template. stationlist = [] for station, stat in zip(stations, statistics): x = {} x['station_id'] = station['station_id'] x['name'] = station['name'] x['coords'] = utils.coords(station['lon'], station['lat']) x['descr'] = station['descr'] x['config'] = station['config'] x['registered'] = station['registered'] x['lastobs'] = stat["last_los"] x['cnt'] = stat["observation_count"] stationlist.append(x) return 'stations.html', dict(stations=stationlist, item_count=station_count)
def test_observations_filters(self, repository: Repository): filters: ObservationFilter = {"obs_id": ObservationId(751)} observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 1) self.assertEqual(observations[0]["obs_id"], 751) # Include observations partially (of fully) in date range # AOS before > LOS after filters = { "aos_before": datetime.datetime(2020, 3, 8, 15, 45, 0, 0), "los_after": datetime.datetime(2020, 3, 8, 15, 30, 0) } observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 1) self.assertEqual(observations[0]["obs_id"], 750) filters = {"sat_id": SatelliteId(28654)} observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 2) self.assertEqual(observations[0]["obs_id"], 1276) self.assertEqual(observations[1]["obs_id"], 752) filters = {"station_id": StationId(1)} observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 4) filters = {"notes": "ote"} observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 1) self.assertEqual(observations[0]["notes"], "Note") filters = {"has_tle": True} observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 2) # obs 751 and 1276 self.assertIsNotNone(observations[0]["tle"]) self.assertIsNotNone(observations[1]["tle"]) filters = { "sat_id": SatelliteId(25338), "station_id": StationId(1), "has_tle": True, "los_after": datetime.datetime(2020, 3, 8, 15, 30, 0) } observations = repository.read_observations(filters=filters) self.assertEqual(len(observations), 1) self.assertEqual(observations[0]["obs_id"], 751)
def migrate(config=None, migration_directory="db"): ''' Perform migrations. Parameters ========== config Dictionary with psycopg2 "connect" method arguments. If None then read INI file migration_directory: str Directory with .psql files. Files must to keep naming convention: svarog-XX.psql where XX is number of database revision. Returns ======= Function print migration status on console. Changes are save in database. Notes ===== If any migration fail then all changes are revert. ''' repository = Repository(config) db_version = repository.get_database_version() migrations = list_migrations(migration_directory) with repository.transaction() as transaction: for migration_version, migration_path in migrations: if migration_version <= db_version: print("Skip migration to %d version" % (migration_version, )) continue print("Process migration to %d version..." % (migration_version, ), end="") with open(migration_path) as migration_file: content = migration_file.read() repository.execute_raw_query(content) print("OK") transaction.commit() new_db_version = repository.get_database_version() print("Migration complete from %d to %d!" % (db_version, new_db_version))
def test_user(self, repository: Repository): """Test if user data can be retrieved automatically.""" nonexistent = repository.read_user(user="******") self.assertIsNone(nonexistent) nonexistent = repository.read_user(user=6) self.assertIsNone(nonexistent) user1 = repository.read_user(user="******") self.assertEqual(user1['username'], 'clarke') self.assertEqual( user1['digest'], 'pbkdf2:sha256:150000$Ij6XJyek$d6a0cd085e6955843a9c3224ccf24088852207d55bb056aa0b544168f94860b8' ) # sha256('password') self.assertEqual(user1['email'], '*****@*****.**') self.assertEqual(user1['role'], UserRole.ADMIN) user2 = repository.read_user(user=3) self.assertEqual(user2['username'], 'clarke') self.assertEqual( user2['digest'], 'pbkdf2:sha256:150000$Ij6XJyek$d6a0cd085e6955843a9c3224ccf24088852207d55bb056aa0b544168f94860b8' ) # sha256('password') self.assertEqual(user2['email'], '*****@*****.**') self.assertEqual(user2['role'], UserRole.ADMIN) self.assertEqual(user1, user2) # UserRole field is enum, better be safe and check all possible combinations. user = repository.read_user(user='******') self.assertEqual(user['role'], UserRole.REGULAR) user = repository.read_user(user='******') self.assertEqual(user['role'], UserRole.OWNER) user = repository.read_user(user='******') self.assertEqual(user['role'], UserRole.BANNED)
def obslist(limit_and_offset, **filters): '''This function retrieves observations list from a local database and displays it.''' aos_before_org = filters.get("aos_before") if aos_before_org is not None: # Repository uses datetime.datetime structure to bound dates and it is # exact date. # User provides datetime.date (which not include hour) and it means that # list should contain observations from @los_after day 00:00:00 hour to # @aos_before day 23:59:59.999 hour. For handle it we add 1 day to # @aos_before day before send to repository. filters["aos_before"] = aos_before_org + timedelta(days=1) repository = Repository() obslist = repository.read_observations(filters, **limit_and_offset) satellites_list = repository.read_satellites() observation_count = repository.count_observations(filters) stations_list = repository.read_stations() satellites_dict = { sat["sat_id"]: sat["sat_name"] for sat in satellites_list } stations_dict = {s["station_id"]: s["name"] for s in stations_list} for obs in obslist: obs["sat_name"] = satellites_dict[obs["sat_id"]] obs["station_name"] = stations_dict[obs["station_id"]] if aos_before_org is not None: # We send back to user the same date as user provide. filters["aos_before"] = aos_before_org # When database will contain many satellites and stations then we need # refactor this code to lazy, async read satellites and stations. return 'obslist.html', dict(obslist=obslist, item_count=observation_count, satellites=satellites_list, stations=stations_list, filters=filters)