Setting Up Application 1 (Web Application)

Application 1 is a web server that exposes API end points that can be consumed by a web, mobile, desktop or a command line application. This section will walk you through the files that make up this application.

Final Directory Structure

Upon completion, the directory structure of the web application will look as follows this:

web-server
├── auth.go
├── main.go
└── web_test.go

The files for this application can be found in this Github repository. You can clone this directly using the following command

git clone https://github.com/demo-apps/semaphore-web-server.git web-server

Application Files Contents

The web application consists of three files:

  1. main.go,
  2. auth.go, and
  3. web_test.go.

1. main.go

The main.go file contains the definition of the HTTP routes and their respective handlers. The contents of this file are as follows:

// main.go (web-server)

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

var auth = authService{Base: "http://localhost:8001"}

func main() {
    gin.SetMode(gin.ReleaseMode)
    s := gin.Default()

    s.POST("/login", login)
    s.GET("/logout", logout)
    s.GET("/protected-content", serveProtectedContent)

    s.Run(":8000")
}

// Handler for the login request
func login(c *gin.Context) {
    // Obtain the POSTed username and password values
    username := c.PostForm("username")
    password := c.PostForm("password")

    if response := auth.Login(username, password); response.Token != "" {
        // If authentication succeeds set the cookies and
        // respond with an HTTP success
        // status and include the token in the response
        c.SetCookie("username", username, 3600, "", "", false, true)
        c.SetCookie("token", response.Token, 3600, "", "", false, true)

        c.JSON(http.StatusOK, response)
    } else {
        // Respond with an HTTP error if authentication fails
        c.AbortWithStatus(http.StatusUnauthorized)
    }
}

// Handler for the logout request
func logout(c *gin.Context) {
    // Obtain the username and token from the cookies
    username, err1 := c.Cookie("username")
    token, err2 := c.Cookie("token")

    if err1 == nil && err2 == nil && auth.Logout(username, token) {
        // Clear the cookies and
        // respond with an HTTP success status
        c.SetCookie("username", "", -1, "", "", false, true)
        c.SetCookie("token", "", -1, "", "", false, true)

        c.JSON(http.StatusOK, nil)
    } else {
        // Respond with an HTTP error
        c.AbortWithStatus(http.StatusUnauthorized)
    }

}

// Handler to serve the protected content
func serveProtectedContent(c *gin.Context) {
    // Obtain the username and token from the cookies
    username, err1 := c.Cookie("username")
    token, err2 := c.Cookie("token")

    if err1 == nil && err2 == nil && auth.Authenticate(username, token) {
        // Respond with an HTTP success status and include the
        // content in the response

        c.JSON(http.StatusOK, gin.H{"content": "This should be visible to authenticated users only."})
    } else {
        // Respond with an HTTP error
        c.AbortWithStatus(http.StatusUnauthorized)
    }
}

2. auth.go

The auth.go file contains the code that interacts with the authentication service. It is this code that we will be testing in our integration tests. The contents of this file are as follows:

// auth.go (api-server)

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/url"
    "strconv"
)

type authService struct {
    Base string
}

type loginResponse struct {
    Token string `json:"token"`
}

func (a *authService) Login(username, password string) loginResponse {
    // Send a login request with the username and password
    _, body, err := post(a.Base+"/login", map[string]string{
        "username": username,
        "password": password,
    })
    lr := loginResponse{}
    if err != nil {
        return lr
    }
    json.Unmarshal(body, &lr)

    return lr
}

func (a *authService) Authenticate(username, token string) bool {
    // Send an authentication request with the username and token
    status, _, _ := post(a.Base+"/authenticate", map[string]string{
        "username": username,
        "token":    token,
    })
    if status == http.StatusOK {
        return true
    }
    return false
}

func (a *authService) Logout(username, token string) bool {
    // Send a logout request with the username and token
    status, _, _ := post(a.Base+"/logout", map[string]string{
        "username": username,
        "token":    token,
    })
    if status == http.StatusOK {
        return true
    }
    return false
}

// Helper function to perform POST requests against the auth server
func post(postURL string, keyValuePairs map[string]string) (int, []byte, error) {
    // Create a form to post with the key value pairs that have been
    // passed in
    form := url.Values{}
    for k, v := range keyValuePairs {
        form.Add(k, v)
    }

    // Create an HTTP Request to post the values
    req, _ := http.NewRequest("POST", postURL, bytes.NewBufferString(form.Encode()))
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return -1, nil, err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return resp.StatusCode, nil, err
    }

    return resp.StatusCode, body, nil
}

3. web_test.go

The web_test.go file contains the integration tests for this application. The contents of this file are as follows:

// web_test.go (web-server)

// +build integration

package main

import "testing"

var a = authService{Base: "http://localhost:8001"}

// User should not be able to login with a wrong username/password
func TestWrongUsernamePassword(t *testing.T) {
    if a.Login("user1", "wrongpassword").Token != "" {
        t.Fail()
    }
}

// User should be able to login with the right username/password
func TestCorrectUsernamePassword(t *testing.T) {
    if a.Login("user1", "pass1").Token == "" {
        t.Fail()
    }
}

// A user's request should be rejected if the user does not
// have a valid session token
func TestInvalidUserRequestAuthentication(t *testing.T) {
    username := "user1"
    lr := a.Login(username, "wrongpassword")
    if a.Authenticate(username, lr.Token) {
        t.Fail()
    }
}

// A user's request should be successfully authenticated if the user
// has a valid session token
func TestUserRequestAuthentication(t *testing.T) {
    username := "user1"
    lr := a.Login(username, "pass1")
    if !a.Authenticate(username, lr.Token) {
        t.Fail()
    }
}

// A user's request should be rejected the user has logged out
func TestUserRequestAuthenticationAfterLoggingOut(t *testing.T) {
    username := "user1"
    // Login
    lr := a.Login(username, "pass1")

    // Test that the user is logged out successfully
    if !a.Logout(username, lr.Token) {
        t.Fail()
    }

    //The user's request after logging out should be rejected
    if a.Authenticate(username, lr.Token) {
        t.Fail()
    }
}

results matching ""

    No results matching ""