Сетевой дневник одного программиста

Персональный блог Константина Огородова

Взаимодействие с Saures API.

Давненько вынашиваю идею поиграться с последней версией .net framework’а и Docker’ом. И так удачно совпало, что есть у меня задача при решении которой я получу практическую пользу. Речь идёт о передаче показаний счётчиков воды из облака Saures в Томский расчётный центр. Немного контекста: Saures — это Российская компания занимающаяся автоматизацией учёта и контроля коммунальных ресурсов. Каталог оборудования компании позволяет организовать очень интересные сценарии. В простейшем случае для учёта потребления воды понадобятся счётчики и контроллер. Контроллер передаёт показания подключенных к нему устройств в «облако» Saures. Доступ к этим данным можно получить в том числе и через API. Именно это я и буду делать в данной статье.

Итак, первым делом я создал решение WaterMeterAutomation и проект библиотеки SauresApi. Для упрощения «общения» с REST API и JSON буду использовать совершенно замечательную библиотеку Flurl. Если быть точнее то Flurl состоит из двух NuGet пакетов: непосредственно Flurl позволяет «строить» (собирать..?) URL, в то время как Flurl.Http — это HTTP-клиент с поддержкой сериализации/десериализации Json. Вот как выглядела первая версия метода LoginAsync в классе Api для вызова метода login API Saures:

public async Task<string> LoginAsync(string email, string password)
{
    var result = await _rootUrl.AppendPathSegment("login")
        .PostUrlEncodedAsync(new {email, password})
        .ReceiveString();
        
    return result;
}

Коротко и наглядно!
Чтобы запустить этот код добавлю в решение XUnit проект интеграционных тестов SauresApi.IntegrationTests. Класс TestApi помечу атрибутом [Trait("Category", "IntegrationTests")], предварительно добавив категорию «IntegrationTests» в настройки Resharper’а Options -> Unit Testing -> General -> Skip tests from categories. Это позволит не запускать интеграционные тесты во время запуска всех тестов решения, поскольку конкретно эти тесты мне нужны как средство отладки во время разработки клиента API и не более.
Код теста максимально прост:

[Fact]
public async Task TestLogin()
{
    var api = await _api.LoginAsync(_suaresApiLogin, _suaresApiPassword);
    api.Should().NotBeNullOrEmpty();
}

Поставив точку останова в нужном месте и запустив тест в режиме отладки я получу возможность посмотреть правильно ли работает мой код и результат возвращаемый сервером, который, кстати сказать, имеет следующее содержание:

{
    "data": {
        "api": 1,
        "role": 1,
        "sid": "92104027-908a-43dd-b18b-b06b918f696d"
    },
    "errors": [],
    "status": "ok"
}

Признаюсь честно: первую объектную обёртку для этого JSON я накидал ручками, но позже вспомнил, что где-то на просторах интернета видел средство создания классов на основе JSON. Первая же ссылка в Google привела на https://json2csharp.com. В дальнейшем все классы я генерировал на этом сайте и затем менял названия.
Далее поковырявшись в документации Saures API пришёл к выводу, что моя конечная цель это метод /object/meters — именно он возвращает список всех измерительных приборов подключенных к контроллеру с их идентификаторами и показаниями. Но для вызова требуется такой параметр как идентификатор объекта, получить который можно вызвав метод /user/objects. Реализация двух вышеупомянутых методов была аналогична методу login и в конечном итоге класс Api стал выглядеть следующим образом:

public class Api
{
    private readonly Url _rootUrl;

    public Api(string host)
    {
        _rootUrl = host.AppendPathSegment("1.0");
    }

    public async Task<string> LoginAsync(string email, string password)
    {
        var result = await _rootUrl.AppendPathSegment("login")
            .PostUrlEncodedAsync(new {email, password})
            .ReceiveJson<Result<LoginData>>();

        result.ThrowExceptionIfAny();

        return result.data.sid;
    }

    public async Task<UserObject> GetUserObjects(string sid)
    {
        var result = await _rootUrl.AppendPathSegment("user").AppendPathSegment("objects")
            .SetQueryParam("sid", sid)
            .GetJsonAsync<Result<UserObjectsData>>();

        result.ThrowExceptionIfAny();

        return result.data.objects[0];
    }

    public async Task<Sensor> GetObjectMeters(string sid, int id)
    {
        var result = await _rootUrl.AppendPathSegment("object").AppendPathSegment("meters")
            .SetQueryParam("sid", sid)
            .SetQueryParam("id", id)
            .GetJsonAsync<Result<SensorsData>>();

        result.ThrowExceptionIfAny();

        return result.data.sensors[0];
    }
}

Метод ThrowExceptionIfAny бросает ApplicationException если массив errors в результирующем JSON не пустой.
Хранение имени пользователя и пароля в тестовом проекте организовал через переменные окружения (Environment variables). Это позволит использовать эти данные только на моей машине и в то же время они не засветятся в открытом виде в Git или другой системе управления версиями. В будущем этот же подход планирую использовать в главном приложении с поддержкой Docker’а. Финальная версия тестового класса:

[Trait("Category", "IntegrationTests")]
public class TestApi
{
    private readonly Api _api;
    private readonly string _suaresApiLogin;
    private readonly string _suaresApiPassword;

    private const string Sid = "0af2b792-2cf9-420c-987e-74e4909c815e";
    private const int Id = 12345;

    public TestApi()
    {
        _suaresApiLogin = Environment.GetEnvironmentVariable("SauresApiLogin", EnvironmentVariableTarget.Process);
        _suaresApiPassword = Environment.GetEnvironmentVariable("SauresApiPassword", EnvironmentVariableTarget.Process);
        _api = new Api("https://api.saures.ru");
    }

    [Fact]
    public async Task TestLogin()
    {
        var api = await _api.LoginAsync(_suaresApiLogin, _suaresApiPassword);
        api.Should().NotBeNullOrEmpty();
    }

    [Fact]
    public async Task TestUserObjects()
    {
        var result = await _api.GetUserObjects(Sid);
        result.id.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task TestGetObjectMeters()
    {
        var result = await _api.GetObjectMeters(Sid, Id);
        result.meters.Should().HaveCount(4);
    }
}

Sid — идентификатор сессии получаемый при вызове Login. Id — идентификатор моего объекта, который вроде бы как постоянен, т.е. метод GetUserObjects можно вызвать единожды.

Отдельно хочу упомянуть про метод Should() в тестах — он принадлежит ещё одному замечательному инструменту Fluent Assertions, который позволяет сделать утверждения в модульных и интеграционных тестах максимально простыми и удобочитаемыми, но в то же время эффективными. Возможно в будущем я остановлюсь на этой теме подробней, поскольку в данном случае потенциала Fluent Assertions попросту не видно.

P.S. в этой статье впервые я использую подсветку синтаксиса кода. Хочу поблагодарить за это Vicky Agravat за разработку дополнения CodeMirror Blocks.

Взаимодействие с Saures API.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Пролистать наверх