Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GWI Platform project #20

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DB_ROOT_PASSWORD=password
DB_NAME=gwi
DB_USER=nero
DB_PASSWORD=password

APP_HOST=localhost
APP_PORT=8080
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore the entire app/logs directory
app/logs/
185 changes: 167 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,180 @@
# GlobalWebIndex Engineering Challenge
# GWI Assets Platform

## Introduction
## Overview

This challenge is designed to give you the opportunity to demonstrate your abilities as a software engineer and specifically your knowledge of the Go language.
GWI Platform is a robust web application that manages user data, assets, and favorites.
It provides RESTful API endpoints for various operations including user management, asset handling, and user favorites.

On the surface the challenge is trivial to solve, however you should choose to add features or capabilities which you feel demonstrate your skills and knowledge the best. For example, you could choose to optimise for performance and concurrency, you could choose to add a robust security layer or ensure your application is highly available. Or all of these.
## Features

Of course, usually we would choose to solve any given requirement with the simplest possible solution, however that is not the spirit of this challenge.
- User management (add)
- Asset management (add, delete, modify, get)
- User favorites handling
- Database integration with MySQL
- Middleware for logging and user authentication
- Graceful server shutdown

## Challenge
## Project Structure

Let's say that in GWI platform all of our users have access to a huge list of assets. We want our users to have a peronal list of favourites, meaning assets that favourite or “star” so that they have them in their frontpage dashboard for quick access. An asset can be one the following
* Chart (that has a small title, axes titles and data)
* Insight (a small piece of text that provides some insight into a topic, e.g. "40% of millenials spend more than 3hours on social media daily")
* Audience (which is a series of characteristics, for that exercise lets focus on gender (Male, Female), birth country, age groups, hours spent daily on social media, number of purchases last month)
e.g. Males from 24-35 that spent more than 3 hours on social media daily.
gwi-platform/
├── README.md
├── app
│ ├── Dockerfile
│ ├── assets
│ │ ├── asset.go
│ │ ├── asset_test.go
│ │ ├── audience.go
│ │ ├── audience_test.go
│ │ ├── chart.go
│ │ ├── chart_test.go
│ │ ├── insight.go
│ │ ├── insight_test.go
│ │ └── queries.go
│ ├── auth
│ │ └── auth.go
│ ├── database
│ │ └── database.go
│ ├── go.mod
│ ├── go.sum
│ ├── handlers
│ │ ├── asset_handlers.go
│ │ ├── favorite_handlers.go
│ │ ├── handlers.go
│ │ ├── queries.go
│ │ └── users_handlers.go
│ ├── html
│ │ ├── api_docs.html
│ │ └── index.html
│ ├── logs
│ │ └── app.log
│ ├── main.go
│ ├── models
│ │ └── models.go
│ ├── server
│ │ ├── app.go
│ │ └── app_test.go
│ └── utils
│ └── utils.go
├── db
│ └── init.sql
├── docker-compose.yml
└── test
├── Dockerfile
├── go.mod
└── main.go

Build a web server which has some endpoint to receive a user id and return a list of all the user’s favourites. Also we want endpoints that would add an asset to favourites, remove it, or edit its description. Assets obviously can share some common attributes (like their description) but they also have completely different structure and data. It’s up to you to decide the structure and we are not looking for something overly complex here (especially for the cases of audiences). There is no need to have/deploy/create an actual database although we would like to discuss about storage options and data representations.

Note that users have no limit on how many assets they want on their favourites so your service will need to provide a reasonable response time.
## Prerequisites

A working server application with functional API is required, along with a clear readme.md. Useful and passing tests would be also be viewed favourably
- Go 1.20 or higher
- MySQL 8.0 or higher

It is appreciated, though not required, if a Dockerfile is included.
## Setup

## Submission
1. Clone the repository:
- git clone https://github.com/your-username/gwi-platform.git
- cd gwi-platform

Just create a fork from the current repo and send it to us!
2. Set up the database:
- Create a MySQL database
- Update the database connection details in `database/database.go`

Good luck, potential colleague!
3. Install dependencies:
- cd app
- go mod tidy

4. Build the application:
- go build -o gwi-platform main.go

5. Run the application:
- ./gwi-platform

## API Endpoints

### User Management

- `POST /user/add`: Add a new user

### Asset Management

- `POST /assets/add`: Add a new asset
- `DELETE /assets/delete`: Delete an asset
- `PUT /assets/modify`: Modify an existing asset
- `GET /assets/get`: Retrieve an asset

### User Favorites

- `POST /user/favorite/add`: Add a favorite for a user
- `GET /user/favorites/`: Get detailed favorites for a user

### Other Endpoints

- `GET /`: Home endpoint
- `GET /docs`: Documentation endpoint
- `GET /ping`: Health check endpoint

## Authentication

The application implements a basic authentication system through the `UserStatusAuth` middleware,
located in the `auth` package. This middleware serves as a secondary authentication layer,
focusing on user status verification rather than primary authentication,
which should be another application eg a CAS.

## Logging

Request and response logging is implemented using the `LogHandler` middleware in the `utils` package.

## Database

The application uses MySQL for data persistence. Database operations are handled in the `database` package.

### Docker Compose

The `docker-compose.yml` file defines the services needed to run the application:

- `app`: The main application service
- `db`: MySQL database service
- `test`: Service for running integration tests

To start the services:
- docker-compose up -d

This will run the integration tests defined in `test/main.go` against the running application.

The integration tests cover:
- User creation
- Asset management (creation, retrieval, modification, deletion)
- User favorites operations

These tests help ensure that all parts of the system are working together as expected in a new environment.


## Future Work

### Enhance Test Coverage
1. **Unit Tests**
- Implement comprehensive unit tests for all packages

2. **Integration Tests**
- Expand current integration test suite to cover more complex scenarios
- Introduce performance tests to ensure scalability
- Rewrite the current integration test suite using a BDD (Behavior-Driven Development).
eg
```gherkin
Feature: User Favorites Management

Scenario: User adds an asset to favorites
Given a user {testUser} exists in the system
And an asset {testAsset} of type "CHART" exists
When the user adds the asset to their favorites
Then the asset should appear in the user's list of favorites
And the total count of user's favorites should increase by 1


### Performance Optimization
1. **Database Optimization**
- Introduce caching mechanisms for frequently accessed data

### Documentation
3. **API Documentation**
- Provide interactive API exploration tools (e.g., Swagger)
16 changes: 16 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM golang:1.20.5-alpine
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for providing a containerized version here. To reduce the image size, could you consider using a multi-stage build? Are there any other techniques you might consider for minimizing the Docker image size?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could either have the app to build before we build the image and copy only the binary, eg if we had this set up in a pipeline build, or we can have a builder image to generate the binary and the copy to the runtime image.


WORKDIR /app

COPY go.mod ./
COPY go.sum ./

RUN go mod download

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]
30 changes: 30 additions & 0 deletions app/assets/asset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package assets

import (
"context"
"gwi-platform/database"
"gwi-platform/models"
)

type Asset interface {
Add(ctx context.Context, db database.DB) error
Delete(ctx context.Context, db database.DB, id models.ID) error
Modify(ctx context.Context, db database.DB) error
Get(ctx context.Context, db database.DB, id models.ID) (interface{}, error)
}

type AssetFactory func() Asset

var assetFactories = map[string]AssetFactory{
"INSIGHT": func() Asset { return &Insight{} },
"CHART": func() Asset { return &Chart{} },
"AUDIENCE": func() Asset { return &Audience{} },
}

func GetAsset(assetType string) (Asset, bool) {
factory, exists := assetFactories[assetType]
if !exists {
return nil, false
}
return factory(), true
}
76 changes: 76 additions & 0 deletions app/assets/asset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package assets

import (
"reflect"
"testing"
)

func TestGetAsset(t *testing.T) {
tests := []struct {
name string
assetType string
wantType reflect.Type
wantOk bool
}{
{
name: "Get Insight Asset",
assetType: "INSIGHT",
wantType: reflect.TypeOf(&Insight{}),
wantOk: true,
},
{
name: "Get Chart Asset",
assetType: "CHART",
wantType: reflect.TypeOf(&Chart{}),
wantOk: true,
},
{
name: "Get Audience Asset",
assetType: "AUDIENCE",
wantType: reflect.TypeOf(&Audience{}),
wantOk: true,
},
{
name: "Get Invalid Asset",
assetType: "INVALID",
wantType: nil,
wantOk: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := GetAsset(tt.assetType)

if ok != tt.wantOk {
t.Errorf("GetAsset() ok = %v, want %v", ok, tt.wantOk)
return
}

if !tt.wantOk {
if got != nil {
t.Errorf("GetAsset() got = %v, want nil", got)
}
return
}

if reflect.TypeOf(got) != tt.wantType {
t.Errorf("GetAsset() got type = %v, want %v", reflect.TypeOf(got), tt.wantType)
}
})
}
}

func TestAssetInterface(t *testing.T) {
assets := []Asset{
&Insight{},
&Chart{},
&Audience{},
}

for _, asset := range assets {
if _, ok := asset.(Asset); !ok {
t.Errorf("%T does not implement Asset interface", asset)
}
}
}
Loading