aboutsummaryrefslogtreecommitdiff
path: root/mobile/mobile_test.go
blob: f497c56f1f5babf98025bbac3dec6b247f7aa92a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package mobile

import (
	"encoding/json"
	"strings"
	"testing"
)

func decodeDay(t *testing.T, s string) map[string]any {
	t.Helper()
	var m map[string]any
	if err := json.Unmarshal([]byte(s), &m); err != nil {
		t.Fatalf("Day returned invalid JSON: %v\n%s", err, s)
	}
	return m
}

func TestDayCarriesLocalisedRank(t *testing.T) {
	pl := decodeDay(t, Day("2026-08-01", "of", "vul", "pl"))
	if pl["rank"] != "memorial" {
		t.Errorf("pl rank = %v, want memorial", pl["rank"])
	}
	if pl["rank_label"] != "wspomnienie obowiązkowe" {
		t.Errorf("pl rank_label = %v", pl["rank_label"])
	}
	en := decodeDay(t, Day("2026-08-01", "of", "vul", "en"))
	if en["rank_label"] != "memorial" {
		t.Errorf("en rank_label = %v", en["rank_label"])
	}
}

// The app shows the colour as a swatch and never as a word, so the raw key must
// be present and no localised label may be. This test exists so the app cannot
// quietly start depending on a colour word.
func TestDayHasColourKeyButNoColourLabel(t *testing.T) {
	m := decodeDay(t, Day("2026-08-01", "of", "vul", "pl"))
	if m["colour"] != "white" {
		t.Errorf("colour = %v, want white", m["colour"])
	}
	if _, present := m["colour_label"]; present {
		t.Error("colour_label must not be returned: the app draws a swatch")
	}
}

func decodeDays(t *testing.T, s string) []map[string]any {
	t.Helper()
	var a []map[string]any
	if err := json.Unmarshal([]byte(s), &a); err != nil {
		t.Fatalf("Days returned invalid JSON: %v\n%s", err, s)
	}
	return a
}

func TestDaysReturnsConsecutiveDates(t *testing.T) {
	a := decodeDays(t, Days("2026-07-27", 7, "of", "pl"))
	if len(a) != 7 {
		t.Fatalf("got %d elements, want 7", len(a))
	}
	want := []string{"2026-07-27", "2026-07-28", "2026-07-29", "2026-07-30",
		"2026-07-31", "2026-08-01", "2026-08-02"}
	for i, w := range want {
		if a[i]["date"] != w {
			t.Errorf("element %d date = %v, want %v", i, a[i]["date"], w)
		}
	}
}

// Days must agree with Day about the same date -- it is a cheaper projection of
// the same computation, not a second implementation.
func TestDaysAgreesWithDay(t *testing.T) {
	one := decodeDay(t, Day("2026-08-01", "of", "vul", "pl"))
	week := decodeDays(t, Days("2026-08-01", 1, "of", "pl"))
	if len(week) != 1 {
		t.Fatalf("got %d elements, want 1", len(week))
	}
	row := week[0]
	for _, k := range []string{"name", "colour", "rank", "rank_label"} {
		if row[k] != one[k] {
			t.Errorf("%s: Days = %v, Day = %v", k, row[k], one[k])
		}
	}
	parts := row["parts"].([]any)
	readings := one["readings"].([]any)
	if len(parts) != len(readings) {
		t.Fatalf("parts = %d, readings = %d", len(parts), len(readings))
	}
	for i := range parts {
		p := parts[i].(map[string]any)
		r := readings[i].(map[string]any)
		if p["part"] != r["part"] || p["citation"] != r["citation"] {
			t.Errorf("element %d: Days %v/%v vs Day %v/%v",
				i, p["part"], p["citation"], r["part"], r["citation"])
		}
	}
}

// No reading text may cross the boundary: that is the whole point of Days.
func TestDaysCarriesNoReadingText(t *testing.T) {
	s := Days("2026-08-01", 7, "of", "pl")
	if strings.Contains(s, `"text"`) {
		t.Error("Days must not return reading text")
	}
	if strings.Contains(s, "colour_label") {
		t.Error("Days must not return a colour label")
	}
}

func TestDaysRejectsBadInput(t *testing.T) {
	for _, c := range []struct {
		start string
		count int
	}{
		{"not-a-date", 7}, {"2026-08-01", 0}, {"2026-08-01", -1}, {"2026-08-01", 400},
		{"2026-08-01", 367}, // one past the documented upper bound
	} {
		if got := Days(c.start, c.count, "of", "pl"); got != "[]" {
			t.Errorf("Days(%q, %d) = %s, want []", c.start, c.count, got)
		}
	}
}

// count's documented range is 1..366; 367 (above) is the first invalid value
// and 366 (here) is the last valid one -- the boundary an off-by-one would
// actually live on.
func TestDaysAcceptsUpperBoundCount(t *testing.T) {
	a := decodeDays(t, Days("2026-01-01", 366, "of", "en"))
	if len(a) != 366 {
		t.Errorf("got %d elements, want 366", len(a))
	}
}

func BenchmarkDaysWeek(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		Days("2026-07-27", 7, "of", "pl")
	}
}

// This is the assertion that would have caught the app's dead 1962 checkboxes:
// it listed nine part IDs where the engine emits two.
func TestPartLabelsMatchesWhatTheEngineEmits(t *testing.T) {
	var ef []map[string]any
	if err := json.Unmarshal([]byte(PartLabels("ef", "pl")), &ef); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	if len(ef) != 2 {
		t.Fatalf("ef: got %d labels, want 2: %v", len(ef), ef)
	}
	if ef[0]["part"] != "epistola" || ef[0]["label"] != "Lekcja" {
		t.Errorf("ef[0] = %v, want epistola/Lekcja", ef[0])
	}
	if ef[1]["part"] != "evangelium" || ef[1]["label"] != "Ewangelia" {
		t.Errorf("ef[1] = %v, want evangelium/Ewangelia", ef[1])
	}

	// OF: derive the expected set from what Days actually emits over a full
	// year (2026 -- a full Sunday cycle, so second readings appear too),
	// rather than hand-copying the production list, which asserts a
	// declaration against itself and can never fail for this class of bug
	// (that is exactly how the app came to render a checkbox -- aklamacja --
	// that filters an ID the engine never emits).
	observed := map[string]bool{}
	s := Days("2026-01-01", 365, "of", "pl")
	var days []map[string]any
	if err := json.Unmarshal([]byte(s), &days); err != nil {
		t.Fatalf("Days returned invalid JSON: %v", err)
	}
	for _, d := range days {
		parts, _ := d["parts"].([]any)
		for _, p := range parts {
			part, _ := p.(map[string]any)
			if id, ok := part["part"].(string); ok {
				observed[id] = true
			}
		}
	}
	if len(observed) == 0 {
		t.Fatal("swept zero part IDs from Days over 2026 -- the sweep is broken, not necessarily the engine")
	}

	var of []map[string]any
	if err := json.Unmarshal([]byte(PartLabels("of", "pl")), &of); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	got := map[string]bool{}
	for _, e := range of {
		got[e["part"].(string)] = true
	}
	if len(got) != len(of) {
		t.Fatalf("PartLabels(of) lists a part ID more than once: %v", of)
	}
	for id := range observed {
		if !got[id] {
			t.Errorf("Days emits part %q somewhere in 2026 but PartLabels(of) does not list it: %v", id, of)
		}
	}
	for id := range got {
		if !observed[id] {
			t.Errorf("PartLabels(of) lists part %q but Days never emits it anywhere in 2026: %v", id, of)
		}
	}
}

func TestPartLabelsUnknownForm(t *testing.T) {
	// An unknown form is treated as the modern one, matching lectByForm: the
	// fallback must be byte-identical to "of", not merely non-empty.
	got := PartLabels("nonsense", "en")
	want := PartLabels("of", "en")
	if got != want {
		t.Errorf("PartLabels(\"nonsense\", \"en\") = %s, want %s (same as \"of\")", got, want)
	}
}