Add tests for handlers and middleware

- drop migration 2022052601_drop_unused_decisions_colums because it was implicitly part of an earlier migration
- add /health endpoint for database health check
- add tests for the health check endpoint
- add tests for middleware secureHeaders, logRequest and tryAuthenticate
- add models.UserModel.CreateUser method
main
Jan Dittberner 2 years ago
parent c3d0733e27
commit 335ce16547

@ -19,6 +19,7 @@ package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -399,3 +400,22 @@ func (app *application) deleteUserForm(_ http.ResponseWriter, _ *http.Request) {
func (app *application) deleteUserSubmit(_ http.ResponseWriter, _ *http.Request) {
panic("not implemented")
}
func (app *application) healthCheck(w http.ResponseWriter, _ *http.Request) {
response := struct {
DB string `json:"database"`
}{DB: "ok"}
enc := json.NewEncoder(w)
w.Header().Set("Content-Type", "application/json")
err := app.motions.DB.Ping()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
response.DB = "FAILED"
}
_ = enc.Encode(response)
}

@ -0,0 +1,92 @@
/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"database/sql"
"net/http"
"net/http/httptest"
"path"
"testing"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.cacert.org/cacert-boardvoting/internal/models"
)
func prepareTestDb(t *testing.T) *sqlx.DB {
t.Helper()
testDir := t.TempDir()
db, err := sql.Open("sqlite3", path.Join(testDir, "test.sqlite"))
require.NoError(t, err)
dbx := sqlx.NewDb(db, "sqlite3")
return dbx
}
func TestApplication_healthCheck(t *testing.T) {
t.Run("check with valid DB", func(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/health", nil)
require.NoError(t, err)
testDB := prepareTestDb(t)
app := &application{
motions: &models.MotionModel{DB: testDB},
}
app.healthCheck(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
})
t.Run("check with broken DB", func(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/health", nil)
require.NoError(t, err)
testDir := t.TempDir()
db, err := sql.Open("sqlite3", path.Join(testDir, "test.sqlite"))
require.NoError(t, err)
testDB := sqlx.NewDb(db, "sqlite3")
_ = db.Close()
app := &application{
motions: &models.MotionModel{DB: testDB},
}
app.healthCheck(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusInternalServerError, rs.StatusCode)
})
}

@ -134,10 +134,7 @@ func (app *application) newTemplateData(
nav topLevelNavItem,
subNav subLevelNavItem,
) *templateData {
user, err := app.GetUser(r)
if err != nil {
app.errorLog.Printf("error getting user for template data: %v", err)
}
user, _ := app.GetUser(r)
return &templateData{
Request: r,

@ -0,0 +1,169 @@
/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.cacert.org/cacert-boardvoting/internal"
"git.cacert.org/cacert-boardvoting/internal/models"
)
func Test_secureHeaders(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
})
secureHeaders(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, "default-src 'self'; font-src 'self' data:", rs.Header.Get("Content-Security-Policy"))
assert.Equal(t, "origin-when-cross-origin", rs.Header.Get("Referrer-Policy"))
assert.Equal(t, "nosniff", rs.Header.Get("X-Content-Type-Options"))
assert.Equal(t, "deny", rs.Header.Get("X-Frame-Options"))
assert.Equal(t, "0", rs.Header.Get("X-XSS-Protection"))
assert.Equal(t, "max-age=63072000", rs.Header.Get("Strict-Transport-Security"))
}
func TestApplication_logRequest(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
r.RemoteAddr = "arg"
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
})
buf := new(bytes.Buffer)
app := &application{infoLog: log.New(buf, "", log.LstdFlags)}
app.logRequest(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
assert.Contains(t, buf.String(), fmt.Sprintf(
"%s - %s %s %s",
r.RemoteAddr,
r.Proto,
r.Method,
r.URL.RequestURI(),
))
}
func TestApplication_tryAuthenticate(t *testing.T) {
db := prepareTestDb(t)
err := internal.InitializeDb(db.DB, log.New(os.Stdout, "", log.LstdFlags))
require.NoError(t, err)
users := &models.UserModel{DB: db}
_, err = users.CreateUser(
context.Background(),
"Test User",
"test@example.org",
[]string{"test@example.org"},
)
var nextCtx context.Context
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("OK"))
nextCtx = r.Context()
})
require.NoError(t, err)
app := application{
users: &models.UserModel{DB: db},
}
t.Run("without TLS", func(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
app.tryAuthenticate(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
assert.Nil(t, nextCtx.Value(ctxUser))
})
t.Run("with TLS no certificate", func(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
r.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{}}
app.tryAuthenticate(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
assert.Nil(t, nextCtx.Value(ctxUser))
})
t.Run("with TLS matching user", func(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
r.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{EmailAddresses: []string{"test@example.org"}}}}
app.tryAuthenticate(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
user := nextCtx.Value(ctxUser)
assert.NotNil(t, user)
userInstance, ok := user.(*models.User)
assert.True(t, ok)
assert.Equal(t, userInstance.Name, "Test User")
})
}

@ -91,6 +91,8 @@ func (app *application) routes() http.Handler {
router.Handler(http.MethodGet, "/users/:id/delete", canManageUsers.ThenFunc(app.deleteUserForm))
router.Handler(http.MethodPost, "/users/:id/delete", canManageUsers.ThenFunc(app.deleteUserSubmit))
router.HandlerFunc(http.MethodGet, "/health", app.healthCheck)
standard := alice.New(app.logRequest, secureHeaders)
return standard.Then(router)

@ -1,3 +0,0 @@
-- drop unused columns majority and quorum from decisions table
ALTER TABLE decisions ADD COLUMN quorum INTEGER;
ALTER TABLE decisions ADD COLUMN majority INTEGER;

@ -1,3 +0,0 @@
-- drop unused columns majority and quorum from decisions table
ALTER TABLE decisions DROP COLUMN quorum;
ALTER TABLE decisions DROP COLUMN majority;

@ -159,3 +159,37 @@ func (m *UserModel) GetRoles(ctx context.Context, user *User) ([]*Role, error) {
return result, nil
}
func (m *UserModel) CreateUser(ctx context.Context, name string, reminder string, emails []string) (int64, error) {
tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil {
return 0, fmt.Errorf("could not start transaction: %w", err)
}
defer func(tx *sqlx.Tx) {
_ = tx.Rollback()
}(tx)
res, err := tx.Exec(`INSERT INTO voters (name, reminder, enabled) VALUES (?, ?, 0)`, name, reminder)
if err != nil {
return 0, fmt.Errorf("could not insert user: %w", err)
}
userID, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("could not get user id: %w", err)
}
for i := range emails {
_, err := tx.Exec(`INSERT INTO emails (voter, address) VALUES (?, ?)`, userID, emails[i])
if err != nil {
return 0, fmt.Errorf("could not insert email %s for voter %s: %w", emails[i], name, err)
}
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("could not commit user transaction: %w", err)
}
return userID, nil
}

Loading…
Cancel
Save