Showing posts with label Angular. Show all posts
Showing posts with label Angular. Show all posts

Tuesday, October 27, 2020

Cypress.io testing with Auth0 and Angular 10

I read the documentation on both the Auth0 site and the Cypress.io site about using the two technologies together with the Angular framework, but I simply couldn't get it to work.

For example, the tutorial on the Auth0 Blog called The Complete Guide to Angular User Authentication with Auth0 got me most of the way there and the same was true for the Cypress.io "real-world app" code and documentation on GitHub (which is written for React BTW).

These two resources (plus some community posts) allowed me to set up all the underpinnings of a working test system but didn't resolve all my issues. In the past, my Angular project exclusively used Google Authentication, so my Cypress tests required me manually login first, and then run all the tests that needed an authenticated user. It wasn't perfect, but it was workable. However, his manual login option did not work with Auth0 (and Angular 10). Google Authentication would not allow the login window to open in an iFrame when running in the context of a Cypress test (i.e., Cypress was automating/controlling the browser).

- Auth0 authenticated tests running successfully in Cypress.io and Angular 10


This is what I ultimately did to get it working:

1.  Switch my Node.js API from Google tokens to Auth0 authentication tokens by following the Auth0 quickstart for Node.js (Express) backends.

2. Set up a test SPA Application in Auth0 with lower security than my production app. This allowed me to enable username/password logins and turn on the Password Grant Types (under Application > Advanced Settings).

3. Follow the Cypress real-world app section called "Cypress Setup for Testing Auth0) to add a Login function to my Cypress.io setup.

Under Cypress > support > commands.ts, I now have the code below. You can also use plain JavaScript of course. The configuration for the variables is found under src > cypress.env.json.

/// <reference types="cypress" />

/// <reference types="jwt-decode" />

import jwt_decode from 'jwt-decode';


Cypress.Commands.add('login', (overrides = {}) => {

  const username = Cypress.env('auth0_username');

  cy.log(`Logging in as ${username}`);

    cy.request({

      method: "POST",

      url: Cypress.env('auth0_url'),

      body: {

        grant_type: 'password',

        username: Cypress.env('auth0_username'),

        password: Cypress.env('auth0_password'),

        audience: Cypress.env('auth0_audience'),

        scope: Cypress.env('auth0_scope'),

        client_id: Cypress.env('auth0_client_id'),

        client_secret: Cypress.env('auth0_client_secret'),  

      },

    }).then(({ body }) => {

      const claims: any = jwt_decode(body.id_token);

      const { nickname, name, picture, updated_at, email, email_verified, sub, exp } = claims;


      const item = {

        body: {

          ...body,

          decodedToken: {

            claims,

            user: {

              nickname,

              name,

              picture,

              updated_at,

              email,

              email_verified,

              sub,

            },

            audience: '',

            client_id: '',

          },

        },

        expiresAt: exp,

      };


    window.localStorage.setItem('auth0Cypress', JSON.stringify(item));

    return body;

  });

});


let LOCAL_STORAGE_MEMORY = {};

Cypress.Commands.add("saveLocalStorageCache", () => {

  Object.keys(localStorage).forEach(key => {

    LOCAL_STORAGE_MEMORY[key] = localStorage[key];

  });

});


Cypress.Commands.add("restoreLocalStorageCache", () => {

  Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {

    localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);

  });

});


4. With this command added to Cypress.io, I can now login programmatically to Auth0 using the following code in my first test. You'll note that the authenticated user details are being written in local storage (but only during testing), and the Auth0 authenticated token is being stored as a cookie--this is the token that I use with my backend API.


describe('Login', () => {

  beforeEach(() => {

    cy.restoreLocalStorageCache();

  });

  

  it('Should successfully login', () => {

    cy.login2()

      .then((resp) => {

        return resp;

      })

      .then((body) => {

        const {access_token, expires_in, id_token} = body;

        const auth0State = {

          nonce: '',

          state: 'some-random-state'

        };


        // write access token to user-token cookie

        cy.setCookie('user-token', access_token);


        const callbackUrl = `/callback#access_token=${access_token}&scope=openid&id_token=${id_token}&expires_in=${expires_in}&token_type=Bearer&state=${auth0State.state}`;

        cy.visit(callbackUrl, {

          onBeforeLoad(win) {

            win.document.cookie = 'com.auth0.auth.some-random-state=' + JSON.stringify(auth0State);

          }

        });

      })

  });

  afterEach(() => {

    cy.saveLocalStorageCache();

  });

});


6. This all seemed to work great, however, Auth0 still would not recognize that the user is authenticated. The Auth0 client would always return false for isAuthenticated. To get around this issue, I had to hack my AuthGuard.

This is what I had before adding Cypress.io:

return this.auth.isAuthenticated$.pipe(

  tap(loggedIn => {

    if (!loggedIn) {

       this.auth.login(state.url);

    }

 })

);


To get around the issue, I simple added another option. If the code is being run by Cypress, I check for the stored user credential and token. I even added a check that it's the right authenticated user, but that's really not needed.


    // @ts-ignore

    if (window.Cypress) {

      const auth0credentials = JSON.parse(localStorage.getItem("auth0Cypress")!);

      const user = auth0credentials.body.decodedToken.user;

      const access_token = auth0credentials.body.access_token;


      if(user.name === 'youtestuser@yourdomain.com' && access_token) {

        return true;

      } else {

        return this.auth.isAuthenticated$.pipe(

          tap(loggedIn => {

            if (!loggedIn) {

              } else {

                this.auth.login(state.url);

              }

            })

          );

      };

    } else {

      return this.auth.isAuthenticated$.pipe(

        tap(loggedIn => {

          if (!loggedIn) {

            this.auth.login(state.url);

          }

        })

      );

    }


Well, I think that's everything. I hope there is a better answer coming as this hack around the AuthGuard is not a prefect solution, but it does let me move forward and that's promising.


Tuesday, September 19, 2017

Google Cloud Platform for a Full-stack Angular Web Application

At first blush, getting a complete Angular web application deployed and running on the Google Cloud Platform (GCP) can be a daunting task. Each quickstart tutorial is reasonable enough, but if you read ahead through all of the various documentation pages you'll need, they can soon begin to snowball.



UPDATE: After I started writing this post, I discovered a Google lab that covers the same topic: Build a Node.js & Angular Web App Using Google Cloud Platform.

I'll just quickly provide an overview of deploying Angular as the front-end app (on Google App Engine) and MySQL with a Node.js API on the backend. MySQL will run on Cloud SQL and Node.js runs on App Engine.

The basic steps involved are:

1. Set up a GCP account and project

2. Download the Angular sample and install the requirements

3. Deploy your Angular front-end app to GCP

4. Set up a MySQL DB on Cloud SQL. I wrote a whole post about setting on MySQL on Google Cloud SQL.

5. Complete the tutorial for using Google Cloud with Node.js

Since these samples aren't linked, you'll have to test the pieces separately until you develop some interaction in your Angular app. You can use Postman to test your Node.js API.

Sunday, August 14, 2016

Angular 2 ng serve or ng build Permissions Error

Update: If you're running macOS, you should just use Homebrew from the start and you'll likely avoid these issues.

If you grab an Angular 2 project from the web, you might find that you run into permissions errors after you install it.

For example, these commands are a common example of a simple project install. (This example uses yarn, but NPM would have similar results.) The addition of 'sudo' is common on Macs, but it can cause the permissions problem.

$ sudo npm install -g angular-cli
$ sudo npm install -g yarn
$ sudo yarn install

The problem occurs when you try to run $ ng serve (or $ ng build); you see an error such as this one:

EACCES: permission denied, open '/Users/cawood/GitHub/test/node_modules/arr-flatten/index.js'
Error: EACCES: permission denied, open '/Users/cawood/GitHub/test/node_modules/arr-flatten/index.js'
    at Error (native)
    at Object.fs.openSync (fs.js:640:18)
    at Object.fs.readFileSync (fs.js:508:33)
    at Object.Module._extensions..js (module.js:578:20)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.require (module.js:497:17)
    at require (internal/module.js:20:19)
    at Object. (/Users/cawood/GitHub/mix/node_modules/arr-diff/index.js:10:15) 
...  

The error occurs because the install ran as the admin user. The quick solution is to give the current user sufficient rights to the folder structure in your project. This example is heavy-handed, but it's fine for prototypes. These commands change the permissions recursively for everything (files and folders) in the test directory which is the one that contains the Angular 2 project.

$ sudo chmod -R +rwx test 
$ cd test 
$ ng serve

Thursday, December 05, 2013

Using Wyzz Web-based HTML editing control in ASP.NET MVC

I recently had to put out a web-based single page application (SPA) on short notice. To make that happen, I knew I had to use some open-source controls. One was the jsTree treeview control (which I wrote about on this blog - Using jsTree with ASP.NET MVC) and another was the Wyzz WYSIWYG web-based editing control for HTML.
From their site, “Wyzz is an ultra-small, very light WYSIWYG (What You See Is What You Get) Editor for use in your web applications. It's written in JavaScript, and is free (as in speech and as in beer) for you to use in your web applications and/or alter to your needs (see the license conditions).
image
Naturally, the first step to add a reference to the wyzz.js script file. Once you have that, you just need to add the control to an HTML <textarea> element. Finally, it’s a simple matter of adding some JavaScript to “make_wyzz” the control.
<script language="JavaScript" type="text/javascript" src="~/Home/wyzz.js"></script>



<textarea name="textEditor" id="textEditor" rows="10" cols="40">No file loaded...</textarea><br />
<script language="javascript1.2">
    make_wyzz('textEditor');
</script> <div ng-controller="EditorCtrl"> <form novalidate class="simple-form"> <button ng-click="saveFileContent()">save</button> </form> </div>

As you can see in the example above, I’ve chosen to use an AngularJS control to define the behaviour of the save button. In the JavaScript I define a server-side controller function (ASP.NET in this case) and I send it the content of the control by accessing the HTML element that the control is using.

$scope.saveFileContent = function () { 
        $http.post('/Home/SaveFileContent', { filePath: document.getElementById("multilingualfile").innerHTML, content: document.getElementById("wysiwyg" + "textEditor").contentWindow.document.body.innerHTML, title: document.getElementById("titleHtml").value })
            .then(
            function (response) {
                alert("File Save Result: " + response.data.Result);
            },
            function (data) {
                alert("Error saving file content");
            }
        );
    }

Update: Here’s the basic format of the server-side part:


[HttpPost]
public ActionResult SaveFileContent(string filePath, string content, string title)
{
    try
    {
        ...
        
        return Json
            (
                new
                {
                    Result = "Success",
                }
            );
    }
    catch (Exception ex)
    {
       ...

        return Json
            (
                new
                {
                    Result = "Error saving content: " + ex.ToString(),
                }
            );
    }
}

To customize your Wyzz controls, you can edit the wyzz.js file. If you have any issues, refer to the Wyzz discussion forum.