package server import ( "bytes" "io" "log" "net/http" "net/http/httptest" "path/filepath" "testing" "cc.tiredsysadmin.keeporg/internal/kdbx" "cc.tiredsysadmin.keeporg/internal/sqldb/sqldbtest" ) // A merge vault must never store a file the server could not afford to open // later: its header is checked before anything is written. func TestMergeVaultRefusesAnOverLimitKDF(t *testing.T) { h := newHarness(t) tok, _, _ := h.login("admin", adminPassword, "") id := h.makeVault(tok, "Merged") saved := kdbx.MaxArgon2Iterations kdbx.MaxArgon2Iterations = 10_000 heavy, err := kdbx.New("Merged", kdbx.KDFParams{MemoryKiB: 8, Iterations: 5_000, Lanes: 1}).Marshal(vaultPassword) kdbx.MaxArgon2Iterations = saved if err != nil { t.Fatal(err) } status, body, _ := h.push(tok, id, `"1"`, heavy) if status != http.StatusBadRequest || body["code"] != "invalid_kdbx" { t.Fatalf("over-limit push: %d %v", status, body) } if v := h.srv.blobs.Versions(id); len(v) != 1 { t.Fatalf("versions after a refused push: %v", v) } } // Two server processes on one database: a push accepted by a process whose // view is stale must not overwrite the version another process committed. func TestStalePushOnAnotherNodeDoesNotOverwrite(t *testing.T) { url := sqldbtest.URL(t) key := filepath.Join(t.TempDir(), "master.key") h := newHarnessWith(t, func(c *Config) { c.Database, c.MasterKeyFile = url, key }) tok, _, _ := h.login("admin", adminPassword, "") _, created := h.call("POST", "/api/v1/databases", tok, map[string]string{ "name": "Shared", "owner_group_id": "everyone", "merge_strategy": "server-wins"}) id := created["id"].(string) if status, body, _ := h.push(tok, id, `"0"`, []byte("version one")); status != http.StatusOK { t.Fatalf("v1: %d %v", status, body) } other, err := New(Config{DataDir: t.TempDir(), Database: url, MasterKeyFile: key, ExternalURL: "http://other"}, log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } defer other.Close() ts := httptest.NewServer(other.Handler()) defer ts.Close() put := func(base, data string) int { req, _ := http.NewRequest("PUT", ts.URL+"/api/v1/databases/"+id+"/blob", bytes.NewReader([]byte(data))) req.Header.Set("Authorization", "Bearer "+tok) req.Header.Set("If-Match", base) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp.StatusCode } if status := put(`"1"`, "version two, by the other node"); status != http.StatusOK { t.Fatalf("v2 on the other node: %d", status) } // The first node has not reloaded yet and still thinks v1 is current. status, body, _ := h.push(tok, id, `"1"`, []byte("a stale edit")) if status != http.StatusConflict { t.Fatalf("stale push: %d %v", status, body) } got, err := h.srv.blobs.Get(id, 2) if err != nil || string(got) != "version two, by the other node" { t.Fatalf("version 2 is now %q (%v)", got, err) } } func TestQueryTokenOnlyOpensTheEventStream(t *testing.T) { h := newHarness(t) tok, _, _ := h.login("admin", adminPassword, "") resp, err := http.Get(h.http.URL + "/api/v1/databases?access_token=" + tok) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("a token in the URL opened /databases: %d", resp.StatusCode) } } func TestAdminEndpointsDoNotRevealDatabases(t *testing.T) { h := newHarness(t) admin, _, _ := h.login("admin", adminPassword, "") id := h.makeVault(admin, "Private") bob := h.makeUser("bob", "bob-password-1") for _, target := range []string{id, "does-not-exist"} { status, _ := h.call("PATCH", "/api/v1/databases/"+target, bob, map[string]string{"name": "x"}) if status != http.StatusNotFound { t.Errorf("PATCH %s as an outsider: %d", target, status) } } }