AL-Go icon indicating copy to clipboard operation
AL-Go copied to clipboard

[Question]: Best practice for Testing BC API's using the build process

Open rolandpthompson opened this issue 1 year ago • 6 comments

Question

We're using more and more Unit Tests, but with regards to how it works with testing APIs (built into BC, either standard or our own custom APIs) in the AL-Go build process I'm a bit stuck. Given we need to authenticate against BC to access a page - how can we do that - is there something I am missing? Just looking for a bit guidance

rolandpthompson avatar Aug 15 '24 15:08 rolandpthompson

Im currently on Vacation so forgive my briefness. But I think our current solution, is to use the hardcoded Username and Password that's used when a new container is created in the pipeline. The only drawback is that your local containers need the same credentials. That was for us the most practical way.

But the best way would probably be to create a new user before running the test. But you would also need to create the user in the database which is probably not easy from within the test.

jonaswre avatar Aug 16 '24 08:08 jonaswre

Thanks, did think of that, but was wondering if I had missed something and maybe there was a more elegant solution 😀

rolandpthompson avatar Aug 16 '24 19:08 rolandpthompson

My thoughts on this was to be able to check-in a postman or like collection to an AL-Go repo - and then we should run this collection against the BC container for testing. It is still in the backlog though.

But... - let me know a little about how you were thinking about running API tests - and also @jonaswre - how are you running API tests?

freddydk avatar Aug 19 '24 08:08 freddydk

@freddydk we are running API test from within Test Codeunits that actually test e2e the API. A typical test would be generate some demo data json. Post it against an endpoint an check if that did create all the entites you expected. Doing this e2e gives as better confidence that things work as expected which is important since API pages are sometimes difficult to work with. (Order of events being triggered etc.)

jonaswre avatar Aug 19 '24 08:08 jonaswre

Got it - so you are writing code to connect to "yourself" using httpClient ? Do you have a simple example of a test codeunit using this?

freddydk avatar Aug 19 '24 09:08 freddydk

as you @freddydk mentioned postman: we kick'd off a small PoC a little while ago to make use of NewMan to process data from Postman. that worked kind of ok :) and allows you to bypass some postman API call limitations.

KristofKlein avatar Sep 18 '24 13:09 KristofKlein

@KristofKlein I recently had this come up again. So I've created a little Codeunit to wrap some useful methods.

codeunit 99990 "BYD BDT Test API Helper"
{
    var
        LibraryRandom: Codeunit "Library - Random";
        LibrarySetupStorage: Codeunit "Library - Setup Storage";
        LibraryTestInitialize: Codeunit "Library - Test Initialize";
        LibraryVariableStorage: Codeunit "Library - Variable Storage";
        WebServiceKey: Text[50];
        UserName: Text[50];

    [Scope('OnPrem')]
    procedure InitializeTest(TestCodeunitId: Integer)
    var
        User: Record User;
        IdentityManagement: Codeunit "Identity Management";
    begin
        EnableHttpClientRequests();
        LibraryTestInitialize.OnTestInitialize(TestCodeunitId);
        ClearLastError();
        LibraryVariableStorage.Clear();
        LibrarySetupStorage.Restore();

        LibraryTestInitialize.OnBeforeTestSuiteInitialize(TestCodeunitId);
        LibraryRandom.Init();

        // Set up web access key for first user
        User.FindFirst();
        WebServiceKey := IdentityManagement.CreateWebServicesKeyNoExpiry(User."User Security ID");
        UserName := User."User Name";

        LibraryTestInitialize.OnAfterTestSuiteInitialize(TestCodeunitId);
    end;

    procedure EnableHttpClientRequests()
    var
        NAVAppSetting: Record "NAV App Setting";
        AppInfo: ModuleInfo;
    begin
        NavApp.GetCurrentModuleInfo(AppInfo);

        NAVAppSetting."App ID" := AppInfo.Id();
        NAVAppSetting."Allow HttpClient Requests" := true;
        if not NAVAppSetting.Insert() then
            NAVAppSetting.Modify();
    end;

    procedure GetBasicAuthHeader(): Text
    var
        Convert: Codeunit "Base64 Convert";
        Credentials: Text;
    begin
        Credentials := StrSubstNo('%1:%2', UserName, WebServiceKey);
        exit(StrSubstNo('Basic %1', Convert.ToBase64(Credentials)));
    end;

    procedure GetUnboundActionUrl(ActionName: Text; Parameters: Dictionary of [Text, Text]): Text
    var
        CompanyInformation: Record "Company Information";
        BaseUrl: Text;
        Parameter: Text;
        UrlBuilder: TextBuilder;
        TenantId: Text;
    begin
        BaseUrl := GetUrl(ClientType::ODataV4);
        CompanyInformation.Get();

        // Extract tenant from base URL
        TenantId := 'default';  // Default value
        if BaseUrl.Contains('tenant=') then
            TenantId := BaseUrl.Substring(BaseUrl.IndexOf('tenant=') + 7);
        if TenantId.Contains('/') then
            TenantId := TenantId.Substring(1, TenantId.IndexOf('/') - 1);

        // Build URL with correct structure
        UrlBuilder.Append(BaseUrl.Substring(1, BaseUrl.IndexOf('?') - 1));  // Base part without query
        UrlBuilder.Append('/');
        UrlBuilder.Append(ActionName);
        UrlBuilder.Append('?tenant=');
        UrlBuilder.Append(TenantId);
        UrlBuilder.Append('&company=');
        UrlBuilder.Append(GetUrlEncodedCompanyName());

        // Add additional parameters
        foreach Parameter in Parameters.Keys() do begin
            UrlBuilder.Append('&');
            UrlBuilder.Append(Parameter);
            UrlBuilder.Append('=');
            UrlBuilder.Append(Parameters.Get(Parameter));
        end;

        exit(UrlBuilder.ToText());
    end;

    procedure GetUrlEncodedCompanyName(): Text
    var
        TypeHelper: Codeunit "Type Helper";
        CompanyN: Text;
    begin
        CompanyN := CompanyName();
        TypeHelper.UrlEncode(CompanyN);
        exit(CompanyN);
    end;

    procedure GetResponseContent(var ResponseMessage: HttpResponseMessage) Content: Text
    var
        InStr: InStream;
    begin
        ResponseMessage.Content.ReadAs(InStr);
        InStr.ReadText(Content);
    end;
}

There is some stuff that you might not need but this might point you into the right direction.

jonaswre avatar Nov 18 '24 17:11 jonaswre