benjivers/notes Public Public: anyone can read this; nobody can change it.

Files b1c728e7eb4bf51bf75a6af39ebff6d562839a67
141 lines · 6.3 KB · ~1.9k tok Raw History
1
import { expect, test } from "bun:test";
2
import { mkdtempSync } from "node:fs";
3
import { tmpdir } from "node:os";
4
import { join } from "node:path";
5
import {
6
  addNote,
7
  BODY_MAX,
8
  cleanTitle,
9
  deleteNote,
10
  findNote,
11
  getNote,
12
  listNotes,
13
  openOrCreate,
14
  searchNotes,
15
  today,
16
  updateNote,
17
} from "./actions.ts";
18
import { openDb } from "./db.ts";
19
import type { Who } from "./who.ts";
20
21
const db = openDb(mkdtempSync(join(tmpdir(), "app-actions-")));
22
const anon: Who = { role: "anon" };
23
const alice: Who = { role: "user", id: "u1", name: "alice" };
24
const bob: Who = { role: "user", id: "u2", name: "bob" };
25
const root: Who = { role: "admin", id: "u0", name: "root" };
26
27
const must = <T>(r: { ok: true; value: T } | { ok: false; message: string }): T => {
28
  if (!r.ok) throw new Error(r.message);
29
  return r.value;
30
};
31
32
test("a vault is its author's: nobody else lists or reads it, admins included", () => {
33
  const refused = addNote(db, anon, { title: "x" });
34
  expect(refused.ok).toBe(false);
35
  if (!refused.ok) expect(refused.error).toBe("signin_required");
36
  const mine = must(addNote(db, alice, { title: "  Secret   plans ", body: "the plan" }));
37
  expect(mine.title).toBe("Secret plans");
38
  expect(mine.author_name).toBe("alice");
39
  expect(must(listNotes(db, alice)).map((n) => n.id)).toContain(mine.id);
40
  expect(must(listNotes(db, bob))).toEqual([]);
41
  expect(must(listNotes(db, root))).toEqual([]);
42
  for (const who of [bob, root]) {
43
    const read = getNote(db, who, { id: mine.id });
44
    expect(read.ok).toBe(false);
45
    if (!read.ok) expect(read.error).toBe("forbidden");
46
    expect(updateNote(db, who, { id: mine.id, body: "mine now" }).ok).toBe(false);
47
    expect(deleteNote(db, who, { id: mine.id }).ok).toBe(false);
48
  }
49
  const listed = listNotes(db, anon);
50
  expect(listed.ok).toBe(false);
51
  // Two people may each have a note of the same name.
52
  expect(addNote(db, bob, { title: "secret plans" }).ok).toBe(true);
53
});
54
55
test("titles are unique in a vault, any case; blank means the next Untitled", () => {
56
  expect(must(addNote(db, alice, {})).title).toBe("Untitled");
57
  expect(must(addNote(db, alice, { title: "" })).title).toBe("Untitled 2");
58
  const dup = addNote(db, alice, { title: "SECRET PLANS" });
59
  expect(dup.ok).toBe(false);
60
  if (!dup.ok) expect(dup.error).toBe("invalid");
61
  for (const bad of ["a [b]", "x|y", "a#b", "^", "x".repeat(201), "   "])
62
    expect([bad, cleanTitle(bad).ok]).toEqual([bad, false]);
63
  expect(cleanTitle("2026-10-02").ok).toBe(true);
64
  const big = addNote(db, alice, { title: "big", body: "x".repeat(BODY_MAX + 1) });
65
  expect(big.ok).toBe(false);
66
});
67
68
test("renaming a note carries every link to it, keeping aliases and headings", () => {
69
  const target = must(addNote(db, alice, { title: "Garden" }));
70
  const a = must(addNote(db, alice, { title: "Links A", body: "see [[Garden]] and [[garden#Beds|the beds]]" }));
71
  const b = must(addNote(db, alice, { title: "Links B", body: "no link, just garden" }));
72
  // Bob's note links a Garden of his own: a rename in alice's vault leaves it be.
73
  const bobs = must(addNote(db, bob, { title: "Bob's", body: "[[Garden]]" }));
74
  const renamed = must(updateNote(db, alice, { id: target.id, title: "Allotment" }));
75
  expect(renamed.title).toBe("Allotment");
76
  expect(renamed.relinked).toBe(1);
77
  expect(must(getNote(db, alice, { id: a.id })).body).toBe(
78
    "see [[Allotment]] and [[Allotment#Beds|the beds]]",
79
  );
80
  expect(must(getNote(db, alice, { id: b.id })).body).toBe("no link, just garden");
81
  expect(must(getNote(db, bob, { id: bobs.id })).body).toBe("[[Garden]]");
82
  // A rename onto a title in use is refused, and changes nothing.
83
  const clash = updateNote(db, alice, { id: target.id, title: "links a" });
84
  expect(clash.ok).toBe(false);
85
  expect(must(getNote(db, alice, { id: target.id })).title).toBe("Allotment");
86
});
87
88
test("pinning floats a note to the top", () => {
89
  const pinned = must(addNote(db, alice, { title: "Pin me" }));
90
  must(addNote(db, alice, { title: "Newer" }));
91
  must(updateNote(db, alice, { id: pinned.id, pinned: true }));
92
  expect(must(listNotes(db, alice))[0]?.id).toBe(pinned.id);
93
  must(updateNote(db, alice, { id: pinned.id, pinned: false }));
94
  expect(must(listNotes(db, alice))[0]?.id).not.toBe(pinned.id);
95
});
96
97
test("following a link opens the note, or makes it", () => {
98
  const made = must(openOrCreate(db, alice, { title: "Reading list" }));
99
  expect(made.created).toBe(true);
100
  const again = must(openOrCreate(db, alice, { title: "reading LIST" }));
101
  expect(again.created).toBe(false);
102
  expect(again.id).toBe(made.id);
103
  expect(must(findNote(db, alice, { title: "Reading list" })).id).toBe(made.id);
104
  expect(findNote(db, bob, { title: "Reading list" }).ok).toBe(false);
105
});
106
107
test("search finds words, phrases and tags, in your vault only", () => {
108
  must(addNote(db, alice, { title: "Recipes", body: "Bread with #cooking/baking\nsour dough starter" }));
109
  must(addNote(db, alice, { title: "Groceries", body: "flour, #cooking" }));
110
  must(addNote(db, bob, { title: "Bob recipes", body: "#cooking" }));
111
  const titles = (q: string) => must(searchNotes(db, alice, { q })).map((h) => h.title);
112
  expect(titles("#cooking").sort()).toEqual(["Groceries", "Recipes"]);
113
  expect(titles("tag:cooking/baking")).toEqual(["Recipes"]);
114
  expect(titles('"sour dough"')).toEqual(["Recipes"]);
115
  expect(titles("dough flour")).toEqual([]);
116
  expect(titles("recipes")).toEqual(["Recipes"]);
117
  expect(must(searchNotes(db, alice, { q: "sour" }))[0]?.snippet).toContain("sour dough");
118
  expect(searchNotes(db, anon, { q: "cooking" }).ok).toBe(false);
119
});
120
121
test("only the author removes a note", () => {
122
  const mine = must(addNote(db, alice, { title: "Ephemeral" }));
123
  const byAnon = deleteNote(db, anon, { id: mine.id });
124
  expect(byAnon.ok).toBe(false);
125
  if (!byAnon.ok) expect(byAnon.error).toBe("signin_required");
126
  expect(deleteNote(db, alice, { id: mine.id }).ok).toBe(true);
127
  const gone = deleteNote(db, alice, { id: mine.id });
128
  expect(gone.ok).toBe(false);
129
  if (!gone.ok) expect(gone.error).toBe("not_found");
130
  const odd = deleteNote(db, alice, { id: 1.5 });
131
  expect(odd.ok).toBe(false);
132
  if (!odd.ok) expect(odd.error).toBe("invalid");
133
});
134
135
test("today is the date where you are", () => {
136
  const at = new Date("2026-10-02T02:00:00Z");
137
  expect(today(0, at)).toBe("2026-10-02");
138
  // Chicago in October is UTC-5: getTimezoneOffset() says 300.
139
  expect(today(300, at)).toBe("2026-10-01");
140
  expect(today(-600, at)).toBe("2026-10-02");
141
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts