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

test: added test on auth #21

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
49 changes: 33 additions & 16 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,44 @@
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
JWT_SECRET: "sample-secret-for-test-to-use"
PG_PASSWORD: "postgres"
PG_HOST: "localhost"
PG_USER: "postgres"
PG_PORT: "5432"
REDIS_ADDR: "localhost"
jobs:

build:
runs-on: ubuntu-latest
services:
redis:
image: redis:latest
ports:
- 6379/tcp
postgres:
image: postgres:latest
env:
POSTGRES_DB: iam
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22.5'

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22.5"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v
12 changes: 9 additions & 3 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func (e APIError) Error() string {
return e.Msg
}

// JSONWriter is helper to return a json response based on status and v
func JSONWriter(w http.ResponseWriter, status int, v any) error {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
Expand All @@ -27,7 +28,7 @@ func makeHandler(f apiFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := f(w, r); err != nil {
if e, ok := err.(APIError); ok {
slog.Error(e.Msg, "status", e.Status)
slog.Error(e.Path, "msg", e.Msg, "status", e.Status)
response := GenericResponse{
Message: err.Error(),
}
Expand All @@ -37,12 +38,15 @@ func makeHandler(f apiFunc) http.HandlerFunc {
}
}

// API details
type API struct {
listenAddr string
database *PostgresDb
memoryCache *redis.Client
}

// APIServer initializes the APIServer including
// its db and memDb
func APIServer(listenAddr, postgresDsn string) *API {
db, err := PostgresCreate(postgresDsn)
if err != nil {
Expand All @@ -60,20 +64,22 @@ func APIServer(listenAddr, postgresDsn string) *API {
}
}

// Create runs the api
func (s *API) Create() {
router := mux.NewRouter()

s.database.db.MustExec(schema)
s.IamRoutes(router)
s.createRoutes(router)

log.Println("iam service running on port", s.listenAddr)
http.ListenAndServe(s.listenAddr, router)
}

func (s *API) IamRoutes(r *mux.Router) {
func (s *API) createRoutes(r *mux.Router) *mux.Router {
userPrefix := r.PathPrefix("/users").Subrouter()
r.HandleFunc("/auth", makeHandler(s.handleAuth)).Methods(http.MethodPost)
r.HandleFunc("/logout", makeHandler(s.handleLogout)).Methods(http.MethodDelete)

userPrefix.HandleFunc("/{id}", UserDetails).Methods(http.MethodGet)
return r
}
22 changes: 17 additions & 5 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ func (s *API) handleAuth(w http.ResponseWriter, r *http.Request) error {
return APIError{
Path: "/auth",
Status: http.StatusBadRequest,
Msg: err.Error(),
Msg: "Missing login details",
}
}

usersMap := make(map[string]*User)
user := User{}
permissions := []string{}

// query to the database to get the user and its roles
userRoles, err := s.database.getUserWithRolesByUsername(login.Username)
if err != nil {
return APIError{
Expand All @@ -60,21 +61,24 @@ func (s *API) handleAuth(w http.ResponseWriter, r *http.Request) error {
permissions = append(permissions, userRole.Role.Permissions)
}

// set user
for _, u := range usersMap {
user = *u
}

// compare the passsword hash using bcrypt
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(login.Password)); err != nil {
return APIError{
Path: "/auth",
Status: http.StatusUnauthorized,
Msg: err.Error(),
Msg: "Invalid login details given",
}
}

// remove duplicate scopes
// remove duplicate scopes from the permissions of the user
scope := RemoveStringDuplicate(permissions)

// create custom claim containing the scope
claims := Claims{
Payload{
Scope: scope,
Expand Down Expand Up @@ -110,10 +114,18 @@ func (s *API) handleLogout(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "application/json")
ctx := context.Background()
// Get the token from the Authorization header
token := r.Header.Get("Authorization")[7:]
token := r.Header.Get("Authorization")

if token == "" {
return APIError{
Path: "/logout",
Status: http.StatusUnauthorized,
Msg: "Authorization header invalid",
}
}

// Extract claim and checks validity
sub, err := authorizer.AuthorizedAccess("user.logout", token)
sub, err := authorizer.AuthorizedAccess("user.logout", token[7:])
if err != nil {
return APIError{
Path: "/logout",
Expand Down
76 changes: 76 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
)

func createServer() *mux.Router {
s := APIServer(":8000", dsn)
r := mux.NewRouter()
s.database.db.MustExec(schema)
return s.createRoutes(r)
}

func TestHandleAuth(t *testing.T) {
s := createServer()

invalidData := []byte(`{"username": "john.doe", "password": ""}`)
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBuffer(invalidData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

s.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status code %v, got %v", http.StatusUnauthorized, w.Code)
}

response := map[string]string{}
json.Unmarshal(w.Body.Bytes(), &response)
if response["message"] != "Invalid login details given" {
t.Errorf("Unexpected body: %v", response)
}

invalidData = []byte(``)
req = httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBuffer(invalidData))
w = httptest.NewRecorder()
s.ServeHTTP(w, req)
json.Unmarshal(w.Body.Bytes(), &response)
if response["message"] != "Missing login details" {
t.Errorf("Unexpected body: %v", response)
}
}

func TestHandleLogout(t *testing.T) {
s := createServer()

invalidData := []byte(`{"username": "john.doe", "password": ""}`)
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBuffer(invalidData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

s.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status code %v, got %v", http.StatusUnauthorized, w.Code)
}

response := map[string]string{}
json.Unmarshal(w.Body.Bytes(), &response)
if response["message"] != "Invalid login details given" {
t.Errorf("Unexpected body: %v", response)
}

invalidData = []byte(``)
req = httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBuffer(invalidData))
w = httptest.NewRecorder()
s.ServeHTTP(w, req)
json.Unmarshal(w.Body.Bytes(), &response)
if response["message"] != "Missing login details" {
t.Errorf("Unexpected body: %v", response)
}
}
Loading