- go.mod module path (and every internal/vibetree import) inventree -> vibetree - cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service - Default site name (seed data + the siteName template func's fallback) is now "vibetree", shown in the navbar, <title>, and login page - Session cookie renamed inventree_session -> vibetree_session - Default DB path data/inventree.db -> data/vibetree.db, CLI/log text, Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all updated to match Existing installs: this changes the session cookie name (users will be signed out once) and the default DB filename (point -db at your existing file, or rename it, to keep your data).
217 lines
5.8 KiB
Go
217 lines
5.8 KiB
Go
package web_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"vibetree/internal/database"
|
|
"vibetree/internal/models"
|
|
"vibetree/internal/web"
|
|
)
|
|
|
|
func testApp(t *testing.T) *web.App {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
db, err := database.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
app, err := web.NewApp(db, log, false)
|
|
if err != nil {
|
|
t.Fatalf("new app: %v", err)
|
|
}
|
|
return app
|
|
}
|
|
|
|
// loginAs signs in and returns the session cookie for subsequent requests.
|
|
func loginAs(t *testing.T, srv *httptest.Server, username, password string) *http.Cookie {
|
|
t.Helper()
|
|
form := "username=" + username + "&password=" + password
|
|
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/login", strings.NewReader(form))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("login request: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect on login, got %d", res.StatusCode)
|
|
}
|
|
for _, c := range res.Cookies() {
|
|
if c.Name == "vibetree_session" {
|
|
return c
|
|
}
|
|
}
|
|
t.Fatal("no session cookie set")
|
|
return nil
|
|
}
|
|
|
|
func TestHealthz(t *testing.T) {
|
|
app := testApp(t)
|
|
srv := httptest.NewServer(app.Router())
|
|
defer srv.Close()
|
|
|
|
res, err := http.Get(srv.URL + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", res.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginAndInventoryPage(t *testing.T) {
|
|
app := testApp(t)
|
|
srv := httptest.NewServer(app.Router())
|
|
defer srv.Close()
|
|
|
|
cookie := loginAs(t, srv, "admin", "admin")
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/inventory", nil)
|
|
req.AddCookie(cookie)
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("get inventory: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", res.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(res.Body)
|
|
if !bytes.Contains(body, []byte("Inventory")) {
|
|
t.Fatalf("expected inventory page content")
|
|
}
|
|
}
|
|
|
|
func TestUnauthenticatedRedirectsToLogin(t *testing.T) {
|
|
app := testApp(t)
|
|
srv := httptest.NewServer(app.Router())
|
|
defer srv.Close()
|
|
|
|
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
|
|
res, err := client.Get(srv.URL + "/inventory")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect to login, got %d", res.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFolderCreateRequiresAdmin(t *testing.T) {
|
|
app := testApp(t)
|
|
srv := httptest.NewServer(app.Router())
|
|
defer srv.Close()
|
|
|
|
// Create a normal (non-admin) user directly via the store.
|
|
normalUser := &models.User{Username: "normie", Role: models.RoleUser, Enabled: true}
|
|
if err := app.Users.Create(t.Context(), normalUser, "normiepass"); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
|
|
cookie := loginAs(t, srv, "normie", "normiepass")
|
|
|
|
payload := `{"name":"Should Fail"}`
|
|
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/app/folders", strings.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.AddCookie(cookie)
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("post folder: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for non-admin folder create, got %d", res.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFolderAndAssetLifecycleAsAdmin(t *testing.T) {
|
|
app := testApp(t)
|
|
srv := httptest.NewServer(app.Router())
|
|
defer srv.Close()
|
|
|
|
cookie := loginAs(t, srv, "admin", "admin")
|
|
|
|
folderID := postJSON(t, srv, cookie, "/app/folders", `{"name":"Main Office"}`)["ID"].(float64)
|
|
|
|
assetPayload := `{"name":"Core Switch","item_type_id":4,"folder_id":` + itoa(int64(folderID)) + `,"status_id":1,"fields":{}}`
|
|
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/app/assets", strings.NewReader(assetPayload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.AddCookie(cookie)
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("post asset: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(res.Body)
|
|
t.Fatalf("expected 200 creating asset, got %d: %s", res.StatusCode, body)
|
|
}
|
|
var envelope struct {
|
|
Data struct {
|
|
AssetID string `json:"asset_id"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if envelope.Data.AssetID != "IT-000001" {
|
|
t.Fatalf("expected auto-generated IT-000001, got %s", envelope.Data.AssetID)
|
|
}
|
|
}
|
|
|
|
func postJSON(t *testing.T, srv *httptest.Server, cookie *http.Cookie, path, payload string) map[string]any {
|
|
t.Helper()
|
|
req, _ := http.NewRequest(http.MethodPost, srv.URL+path, strings.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.AddCookie(cookie)
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("post %s: %v", path, err)
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(res.Body)
|
|
t.Fatalf("expected 200 for %s, got %d: %s", path, res.StatusCode, body)
|
|
}
|
|
var envelope struct {
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
return envelope.Data
|
|
}
|
|
|
|
func itoa(n int64) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
var b []byte
|
|
for n > 0 {
|
|
b = append([]byte{byte('0' + n%10)}, b...)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
b = append([]byte{'-'}, b...)
|
|
}
|
|
return string(b)
|
|
}
|