MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
- Demo: senders for mail, Discord and webhooks refuse, no alarm ticker; test alarm, SMTP test, server settings, backups, account and session changes of the shared demo accounts answer 403; the session list only shows the own device. The UI shows the notices and disables the controls. - SMTP password is never sent back (smtpPassSet), empty keeps it. - ?token= on webhook ingest accepts only API keys. - Request bodies capped at 2 MB (sync 8 MB, imports 101 MB). - Deleting an account removes its server backups. - Backup downloads, exports and import uploads get 30 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
5.1 KiB
Go
114 lines
5.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sandrodach/mindsift/internal/config"
|
|
"github.com/sandrodach/mindsift/internal/storage"
|
|
"github.com/sandrodach/mindsift/internal/ws"
|
|
)
|
|
|
|
func TestDemoSendsNothingAndKeepsAccounts(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "demo.db")
|
|
store, err := storage.NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
if err := store.ResetAndSeedDemo(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
|
|
router := srv.Router()
|
|
token := sessionLogin(t, router, "admin", "admin", "visitor-1")
|
|
_ = sessionLogin(t, router, "admin", "admin", "visitor-2")
|
|
|
|
if err := srv.sendCustomWebhookNotification("https://example.com/hook", "x", "x", "", "", ""); err != errDemoOutgoing {
|
|
t.Fatalf("webhook sender must refuse in demo, got %v", err)
|
|
}
|
|
if err := srv.sendDiscordNotification("https://discord.com/api/webhooks/1/x", "x", "", "", ""); err != errDemoOutgoing {
|
|
t.Fatalf("discord sender must refuse in demo, got %v", err)
|
|
}
|
|
if err := srv.sendEmailNotification("a@example.com", "x", "x"); err != errDemoOutgoing {
|
|
t.Fatalf("mail sender must refuse in demo, got %v", err)
|
|
}
|
|
blocked := []struct{ method, path, body string }{
|
|
{"POST", "/api/v1/auth/alarms/test", `{"channel":"webhook","target":"https://example.com/hook"}`},
|
|
{"PUT", "/api/v1/admin/settings", `{"allowPrivateWebhooks":true}`},
|
|
{"POST", "/api/v1/admin/smtp/test", `{"toEmail":"a@example.com"}`},
|
|
{"POST", "/api/v1/admin/backups", ``},
|
|
{"POST", "/api/v1/auth/revoke-all", ``},
|
|
{"DELETE", "/api/v1/user/account", `{"password":"admin"}`},
|
|
{"DELETE", "/api/v1/admin/users/" + storage.DemoUserID, ``},
|
|
{"PATCH", "/api/v1/auth/settings", `{"password":"neuesPasswort1","currentPassword":"admin"}`},
|
|
}
|
|
for _, b := range blocked {
|
|
rec := sessionCall(router, b.method, b.path, token, b.body, "")
|
|
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "Demo") {
|
|
t.Fatalf("%s %s: want 403 demo notice, got %d %s", b.method, b.path, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
// Harmless profile changes still work
|
|
if rec := sessionCall(router, "PATCH", "/api/v1/auth/settings", token, `{"displayName":"Besucher"}`, ""); rec.Code != http.StatusOK {
|
|
t.Fatalf("display name in demo: %d", rec.Code)
|
|
}
|
|
// Visitors only see their own device, not the others' addresses
|
|
var list []storage.Session
|
|
_ = json.Unmarshal(sessionCall(router, "GET", "/api/v1/auth/sessions", token, "", "").Body.Bytes(), &list)
|
|
if len(list) != 1 || !list[0].Current {
|
|
t.Fatalf("demo session list must only show the own device, got %+v", list)
|
|
}
|
|
}
|
|
|
|
func TestSMTPPasswordNeverReturned(t *testing.T) {
|
|
srv, store, token := setupTestServer(t)
|
|
router := srv.Router()
|
|
rec := sessionCall(router, "PUT", "/api/v1/admin/settings", token,
|
|
`{"sharingMode":"organizations","smtpHost":"mail.example.com","smtpUser":"bot","smtpPass":"geheim123"}`, "")
|
|
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), "geheim123") || !strings.Contains(rec.Body.String(), `"smtpPassSet":true`) {
|
|
t.Fatalf("save: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if body := sessionCall(router, "GET", "/api/v1/admin/settings", token, "", "").Body.String(); strings.Contains(body, "geheim123") {
|
|
t.Fatalf("GET leaks the password: %s", body)
|
|
}
|
|
// Saving again with the empty field keeps it
|
|
sessionCall(router, "PUT", "/api/v1/admin/settings", token, `{"sharingMode":"organizations","smtpHost":"mail.example.com","smtpUser":"bot","smtpPass":""}`, "")
|
|
if sys, _ := store.GetSystemSettings(); sys.SMTPPass != "geheim123" {
|
|
t.Fatalf("empty field must keep the password, got %q", sys.SMTPPass)
|
|
}
|
|
// Without SMTP user the password is dropped
|
|
sessionCall(router, "PUT", "/api/v1/admin/settings", token, `{"sharingMode":"organizations","smtpHost":"mail.example.com","smtpUser":"","smtpPass":""}`, "")
|
|
if sys, _ := store.GetSystemSettings(); sys.SMTPPass != "" {
|
|
t.Fatalf("password without user must be cleared, got %q", sys.SMTPPass)
|
|
}
|
|
}
|
|
|
|
func TestDeletedAccountLosesServerBackups(t *testing.T) {
|
|
srv, store, token := setupTestServer(t)
|
|
root := srv.backupRoot()
|
|
gone, _ := newTestUser(t, store, "gone@example.com")
|
|
stays, _ := newTestUser(t, store, "stays@example.com")
|
|
for _, id := range []string{gone.ID, stays.ID} {
|
|
dir := storage.UserBackupDir(root, id)
|
|
_ = os.MkdirAll(dir, 0o755)
|
|
_ = os.WriteFile(filepath.Join(dir, "backup.zip"), []byte("x"), 0o600)
|
|
}
|
|
if rec := sessionCall(srv.Router(), "DELETE", "/api/v1/admin/users/"+gone.ID, token, "", ""); rec.Code != http.StatusOK {
|
|
t.Fatalf("delete user: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if _, err := os.Stat(storage.UserBackupDir(root, gone.ID)); !os.IsNotExist(err) {
|
|
t.Fatalf("backups of the deleted account must be gone: %v", err)
|
|
}
|
|
for _, odd := range []string{"", ".", "/", "..", "../users"} {
|
|
srv.removeUserBackups(odd)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(storage.UserBackupDir(root, stays.ID), "backup.zip")); err != nil {
|
|
t.Fatalf("other accounts' backups must stay: %v", err)
|
|
}
|
|
}
|