summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/src/main/AndroidManifest.xml7
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt20
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt37
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt145
-rw-r--r--cmd/dashboard/main.go1
-rw-r--r--internal/api/google_calendar.go98
-rw-r--r--internal/api/google_calendar_test.go30
-rw-r--r--internal/api/interfaces.go1
-rw-r--r--internal/handlers/timeline_logic_test.go7
-rw-r--r--internal/handlers/widget.go22
-rw-r--r--internal/handlers/widget_test.go47
11 files changed, 10 insertions, 405 deletions
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 74d19ea..ca96e71 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -33,13 +33,6 @@
android:exported="false"
android:windowSoftInputMode="adjustResize" />
- <!-- Event detail bottom sheet (opened by widget calendar event taps) -->
- <activity
- android:name=".ui.EventDetailActivity"
- android:theme="@style/Theme.TaskDetail"
- android:taskAffinity="${applicationId}.eventdetail"
- android:exported="false" />
-
<!-- Settings activity (also the widget config screen) -->
<activity
android:name=".ui.SettingsActivity"
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt
index 3574eb5..134e3bc 100644
--- a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt
+++ b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt
@@ -19,9 +19,6 @@ private data class UpdateDescriptionRequest(val id: String, val source: String,
@Serializable
private data class WidgetAddRequest(val title: String)
-@Serializable
-private data class RecurrenceResponse(val recurrence: String)
-
/**
* Optimistically drops an item from the cached list so the widget can update instantly on
* completion, ahead of the authoritative refresh a worker performs once the API call succeeds.
@@ -148,21 +145,4 @@ class WidgetRepository(
check(response.isSuccessful) { "HTTP ${response.code}" }
}
}
-
- /** GETs the formatted recurrence schedule for a recurring event. */
- suspend fun getRecurrence(recurringEventId: String): Result<String> =
- withContext(Dispatchers.IO) {
- val encodedId = java.net.URLEncoder.encode(recurringEventId, "UTF-8")
- val request = Request.Builder()
- .url("$serverUrl/api/widget/recurrence?recurring_event_id=$encodedId")
- .header("Authorization", "Bearer $token")
- .build()
- runCatching {
- val response = client.newCall(request).execute()
- check(response.isSuccessful) { "HTTP ${response.code}" }
- val body = checkNotNull(response.body?.string()) { "Empty body" }
- val parsed = json.decodeFromString<RecurrenceResponse>(body)
- parsed.recurrence
- }
- }
}
diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt
index 85a93d6..1983383 100644
--- a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt
+++ b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt
@@ -2,6 +2,7 @@ package org.terst.doot.widget.ui
import android.content.Context
import android.content.Intent
+import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@@ -37,6 +38,12 @@ fun sourceColor(source: String): Color = when (source) {
else -> Color(0xFF888888)
}
+/** Opens the event's source URL (Google Calendar, Plan to Eat, etc.) directly. */
+fun calendarViewIntent(url: String): Intent =
+ Intent(Intent.ACTION_VIEW, Uri.parse(url.ifEmpty { "https://calendar.google.com" })).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
private val json = Json { ignoreUnknownKeys = true }
class DootWidget : GlanceAppWidget() {
@@ -154,20 +161,12 @@ fun WidgetRoot(items: List<WidgetItem>, now: Instant, isRefreshing: Boolean, tex
@Composable
fun AllDayRow(event: WidgetItem, textSize: WidgetTextSize) {
- val context = LocalContext.current
- val detailIntent = Intent(context, EventDetailActivity::class.java).apply {
- putExtra(EventDetailActivity.EXTRA_TITLE, event.title)
- putExtra(EventDetailActivity.EXTRA_SOURCE, event.source)
- putExtra(EventDetailActivity.EXTRA_URL, event.url)
- putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
val color = sourceColor(event.source)
Row(
modifier = GlanceModifier
.fillMaxWidth()
.padding(vertical = 3.dp)
- .clickable(actionStartActivity(detailIntent)),
+ .clickable(actionStartActivity(calendarViewIntent(event.url))),
verticalAlignment = Alignment.CenterVertically
) {
Box(modifier = GlanceModifier.width(3.dp).height(16.dp).background(color)) {}
@@ -291,21 +290,13 @@ fun HourRow(
@Composable
fun EventBlock(event: WidgetItem, isPast: Boolean, textSize: WidgetTextSize) {
- val context = LocalContext.current
- val detailIntent = Intent(context, EventDetailActivity::class.java).apply {
- putExtra(EventDetailActivity.EXTRA_TITLE, event.title)
- putExtra(EventDetailActivity.EXTRA_SOURCE, event.source)
- putExtra(EventDetailActivity.EXTRA_URL, event.url)
- putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
val color = sourceColor(event.source)
val alpha = if (isPast) 0.5f else 1f
Row(
modifier = GlanceModifier
.fillMaxWidth()
.padding(vertical = 4.dp)
- .clickable(actionStartActivity(detailIntent)),
+ .clickable(actionStartActivity(calendarViewIntent(event.url))),
verticalAlignment = Alignment.CenterVertically
) {
Box(
@@ -453,14 +444,6 @@ fun TomorrowSection(items: List<WidgetItem>, fragments: List<TaskFragment>, allD
@Composable
fun TomorrowEventRow(event: WidgetItem, zone: ZoneId, textSize: WidgetTextSize) {
- val context = LocalContext.current
- val detailIntent = Intent(context, EventDetailActivity::class.java).apply {
- putExtra(EventDetailActivity.EXTRA_TITLE, event.title)
- putExtra(EventDetailActivity.EXTRA_SOURCE, event.source)
- putExtra(EventDetailActivity.EXTRA_URL, event.url)
- putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
val color = sourceColor(event.source)
val timeLabel = event.start?.let {
val t = Instant.parse(it).atZone(zone)
@@ -470,7 +453,7 @@ fun TomorrowEventRow(event: WidgetItem, zone: ZoneId, textSize: WidgetTextSize)
modifier = GlanceModifier
.fillMaxWidth()
.padding(vertical = 3.dp)
- .clickable(actionStartActivity(detailIntent)),
+ .clickable(actionStartActivity(calendarViewIntent(event.url))),
verticalAlignment = Alignment.CenterVertically
) {
Text(
diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt
deleted file mode 100644
index b8423ce..0000000
--- a/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-package org.terst.doot.widget.ui
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import okhttp3.OkHttpClient
-import org.terst.doot.widget.data.Keys
-import org.terst.doot.widget.data.WidgetRepository
-import org.terst.doot.widget.data.dataStore
-
-class EventDetailActivity : ComponentActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val title = intent.getStringExtra(EXTRA_TITLE) ?: ""
- val source = intent.getStringExtra(EXTRA_SOURCE) ?: "calendar"
- val url = intent.getStringExtra(EXTRA_URL) ?: ""
- val recurringEventId = intent.getStringExtra(EXTRA_RECURRING_EVENT_ID)
-
- setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- EventDetailSheet(
- title = title,
- source = source,
- recurringEventId = recurringEventId,
- onOpenCalendar = {
- startActivity(
- Intent(Intent.ACTION_VIEW, Uri.parse(url.ifEmpty { "https://calendar.google.com" }))
- )
- finish()
- },
- onDismiss = ::finish
- )
- }
- }
- }
-
- companion object {
- const val EXTRA_TITLE = "event_title"
- const val EXTRA_SOURCE = "event_source"
- const val EXTRA_URL = "event_url"
- const val EXTRA_RECURRING_EVENT_ID = "event_recurring_id"
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun EventDetailSheet(
- title: String,
- source: String,
- recurringEventId: String?,
- onOpenCalendar: () -> Unit,
- onDismiss: () -> Unit
-) {
- val context = LocalContext.current
- var recurrenceText by remember { mutableStateOf<String?>(null) }
-
- LaunchedEffect(recurringEventId) {
- if (recurringEventId == null) return@LaunchedEffect
- recurrenceText = "Loading…"
- val prefs = context.dataStore.data.first()
- val url = prefs[Keys.SERVER_URL]?.trimEnd('/') ?: return@LaunchedEffect
- val token = prefs[Keys.TOKEN] ?: return@LaunchedEffect
- val repo = WidgetRepository(OkHttpClient(), url, token)
- repo.getRecurrence(recurringEventId).onSuccess { text ->
- recurrenceText = text
- }.onFailure {
- recurrenceText = null
- }
- }
-
- ModalBottomSheet(
- onDismissRequest = onDismiss,
- sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
- containerColor = Color(0xFF1E293B),
- tonalElevation = 0.dp,
- dragHandle = {
- Box(
- modifier = Modifier
- .padding(vertical = 12.dp)
- .width(36.dp)
- .height(4.dp)
- .background(Color.White.copy(alpha = 0.25f), RoundedCornerShape(2.dp))
- )
- }
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 20.dp)
- .navigationBarsPadding()
- ) {
- Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
- Box(
- modifier = Modifier
- .size(10.dp)
- .background(sourceColor(source), RoundedCornerShape(5.dp))
- )
- Spacer(Modifier.width(10.dp))
- Text(
- text = title,
- fontSize = 17.sp,
- fontWeight = FontWeight.SemiBold,
- color = Color.White,
- modifier = Modifier.weight(1f)
- )
- }
- recurrenceText?.let { text ->
- Spacer(Modifier.height(8.dp))
- Text(
- text = text,
- fontSize = 13.sp,
- color = Color.White.copy(alpha = 0.6f)
- )
- }
- Spacer(Modifier.height(20.dp))
- OutlinedButton(
- onClick = onOpenCalendar,
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White),
- border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.3f))
- ) {
- Text("Open in Calendar", fontSize = 15.sp)
- }
- Spacer(Modifier.height(20.dp))
- }
- }
-}
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index d04a60e..eced067 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -373,7 +373,6 @@ func main() {
r.With(widgetAuth).Post("/api/widget/update", h.HandleWidgetUpdate)
r.With(widgetAuth).Post("/api/widget/complete", h.HandleWidgetComplete)
r.With(widgetAuth).Post("/api/widget/reschedule", h.HandleWidgetReschedule)
- r.With(widgetAuth).Get("/api/widget/recurrence", h.HandleWidgetRecurrence)
r.With(widgetAuth).Post("/api/widget/add", h.HandleWidgetAdd)
} else {
log.Println("WIDGET_TOKEN not set — /api/widget disabled")
diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go
index bb17812..f441a45 100644
--- a/internal/api/google_calendar.go
+++ b/internal/api/google_calendar.go
@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"sort"
- "strconv"
"strings"
"time"
@@ -84,86 +83,6 @@ func deduplicateEvents(events []models.CalendarEvent) []models.CalendarEvent {
return unique
}
-var recurrenceFreqNames = map[string]string{
- "DAILY": "daily",
- "WEEKLY": "weekly",
- "MONTHLY": "monthly",
- "YEARLY": "yearly",
-}
-
-var recurrenceFreqUnits = map[string]string{
- "DAILY": "day",
- "WEEKLY": "week",
- "MONTHLY": "month",
- "YEARLY": "year",
-}
-
-var recurrenceWeekdayNames = map[string]string{
- "SU": "Sunday",
- "MO": "Monday",
- "TU": "Tuesday",
- "WE": "Wednesday",
- "TH": "Thursday",
- "FR": "Friday",
- "SA": "Saturday",
-}
-
-// formatRecurrence turns Google Calendar RRULE strings into short English.
-// This intentionally covers only the common cases (FREQ, INTERVAL, BYDAY) --
-// not a full RFC 5545 parser. Anything it can't confidently describe falls
-// back to "Recurring event" rather than showing nothing or an error.
-func formatRecurrence(rrules []string) string {
- for _, rule := range rrules {
- rule = strings.TrimPrefix(rule, "RRULE:")
- parts := make(map[string]string)
- for _, kv := range strings.Split(rule, ";") {
- pieces := strings.SplitN(kv, "=", 2)
- if len(pieces) == 2 {
- parts[pieces[0]] = pieces[1]
- }
- }
-
- freqKey := parts["FREQ"]
- unit, ok := recurrenceFreqUnits[freqKey]
- if !ok {
- continue
- }
-
- interval := 1
- if iv := parts["INTERVAL"]; iv != "" {
- if n, err := strconv.Atoi(iv); err == nil && n > 0 {
- interval = n
- }
- }
-
- var phrase string
- if interval == 1 {
- phrase = "Repeats " + recurrenceFreqNames[freqKey]
- } else {
- phrase = fmt.Sprintf("Repeats every %d %ss", interval, unit)
- }
-
- if byday := parts["BYDAY"]; byday != "" {
- var days []string
- for _, code := range strings.Split(byday, ",") {
- code = strings.TrimSpace(code)
- if len(code) >= 2 {
- code = code[len(code)-2:]
- }
- if name, ok := recurrenceWeekdayNames[code]; ok {
- days = append(days, name)
- }
- }
- if len(days) > 0 {
- phrase += " on " + strings.Join(days, ", ")
- }
- }
-
- return phrase
- }
- return "Recurring event"
-}
-
// NewGoogleCalendarClient creates a client that fetches from multiple calendars.
// calendarIDs can be comma-separated (e.g., "cal1@group.calendar.google.com,cal2@group.calendar.google.com")
// timezone is the IANA timezone name for display (e.g., "Pacific/Honolulu")
@@ -285,20 +204,3 @@ func (c *GoogleCalendarClient) GetCalendarList(ctx context.Context) ([]models.Ca
return calendars, nil
}
-// GetRecurrenceRule looks up a recurring event's master record and returns
-// its formatted recurrence schedule. Google's API only puts the RRULE on
-// the master event, not on expanded instances (see parseEventTime's
-// SingleEvents(true) callers), so this does a live lookup by the instance's
-// RecurringEventId. There's no per-event calendar attribution stored today
-// (events from all configured calendars are merged without recording which
-// one they came from), so this tries each configured calendar in turn.
-func (c *GoogleCalendarClient) GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) {
- for _, calendarID := range c.calendarIDs {
- event, err := c.srv.Events.Get(calendarID, recurringEventID).Do()
- if err != nil {
- continue
- }
- return formatRecurrence(event.Recurrence), nil
- }
- return "", fmt.Errorf("recurring event %s not found on any configured calendar", recurringEventID)
-}
diff --git a/internal/api/google_calendar_test.go b/internal/api/google_calendar_test.go
index efbab0d..3cf0dbd 100644
--- a/internal/api/google_calendar_test.go
+++ b/internal/api/google_calendar_test.go
@@ -272,33 +272,3 @@ func TestGetUpcomingEvents_APIError_ReturnsEmptyNotError(t *testing.T) {
t.Errorf("expected 0 events on API error, got %d", len(events))
}
}
-
-// --- formatRecurrence ---
-
-func TestFormatRecurrence(t *testing.T) {
- tests := []struct {
- name string
- rules []string
- want string
- }{
- {"weekly single day", []string{"RRULE:FREQ=WEEKLY;BYDAY=MO"}, "Repeats weekly on Monday"},
- {"weekly multiple days", []string{"RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"}, "Repeats weekly on Monday, Wednesday, Friday"},
- {"daily", []string{"RRULE:FREQ=DAILY"}, "Repeats daily"},
- {"monthly", []string{"RRULE:FREQ=MONTHLY"}, "Repeats monthly"},
- {"yearly", []string{"RRULE:FREQ=YEARLY"}, "Repeats yearly"},
- {"interval weekly", []string{"RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU"}, "Repeats every 2 weeks on Sunday"},
- {"interval daily", []string{"RRULE:FREQ=DAILY;INTERVAL=3"}, "Repeats every 3 days"},
- {"unrecognized frequency", []string{"RRULE:FREQ=HOURLY"}, "Recurring event"},
- {"empty", []string{}, "Recurring event"},
- {"unparseable", []string{"not a valid rule"}, "Recurring event"},
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- got := formatRecurrence(tc.rules)
- if got != tc.want {
- t.Errorf("formatRecurrence(%v) = %q, want %q", tc.rules, got, tc.want)
- }
- })
- }
-}
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index 3c1e4e1..183f3f0 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -30,7 +30,6 @@ type GoogleCalendarAPI interface {
GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error)
GetCalendarList(ctx context.Context) ([]models.CalendarInfo, error)
SetCalendarIDs(ids []string)
- GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error)
}
// GoogleTasksAPI defines the interface for Google Tasks operations
diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go
index 1da2d8a..01ad814 100644
--- a/internal/handlers/timeline_logic_test.go
+++ b/internal/handlers/timeline_logic_test.go
@@ -23,9 +23,6 @@ type MockCalendarClient struct {
// with, for tests that need to assert on how the caller resolved its
// calendar ID list (e.g. fetchCalendarEvents' comma-split fallback).
SetCalendarIDsCalls [][]string
- // RecurrenceRule is returned by GetRecurrenceRule for any id when RecurrenceErr is nil.
- RecurrenceRule string
- RecurrenceErr error
}
func (m *MockCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) {
@@ -44,10 +41,6 @@ func (m *MockCalendarClient) SetCalendarIDs(ids []string) {
m.SetCalendarIDsCalls = append(m.SetCalendarIDsCalls, ids)
}
-func (m *MockCalendarClient) GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) {
- return m.RecurrenceRule, m.RecurrenceErr
-}
-
func setupTestStore(t *testing.T) *store.Store {
t.Helper()
tempDir := t.TempDir()
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index f1f7452..0092b6b 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -359,28 +359,6 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-// HandleWidgetRecurrence looks up and formats a recurring calendar event's schedule.
-func (h *Handler) HandleWidgetRecurrence(w http.ResponseWriter, r *http.Request) {
- recurringEventID := r.URL.Query().Get("recurring_event_id")
- if recurringEventID == "" {
- http.Error(w, "recurring_event_id is required", http.StatusBadRequest)
- return
- }
-
- recurrence, err := h.googleCalendarClient.GetRecurrenceRule(r.Context(), recurringEventID)
- if err != nil {
- http.Error(w, "recurring event not found", http.StatusNotFound)
- return
- }
-
- resp := struct {
- Recurrence string `json:"recurrence"`
- }{Recurrence: recurrence}
-
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(resp)
-}
-
// HandleWidgetAdd creates a new undated native task from the widget's quick-add sheet.
func (h *Handler) HandleWidgetAdd(w http.ResponseWriter, r *http.Request) {
var req widgetAddRequest
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 3116918..4e08d2a 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -3,7 +3,6 @@ package handlers
import (
"context"
"encoding/json"
- "fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -788,49 +787,3 @@ func TestTimelineItemToWidgetItem_NonRecurring_EmptyRecurringEventID(t *testing.
}
}
-// TestHandleWidgetRecurrence_ReturnsFormattedSchedule proves the recurrence
-// lookup endpoint: given a recurring_event_id query param, it calls the
-// calendar client's GetRecurrenceRule and returns the formatted text.
-func TestHandleWidgetRecurrence_ReturnsFormattedSchedule(t *testing.T) {
- mock := &MockCalendarClient{RecurrenceRule: "Repeats weekly on Monday"}
- h := &Handler{googleCalendarClient: mock}
- req := httptest.NewRequest("GET", "/api/widget/recurrence?recurring_event_id=master-1", nil)
- w := httptest.NewRecorder()
- http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected 200, got %d", w.Code)
- }
- var resp struct {
- Recurrence string `json:"recurrence"`
- }
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("failed to decode response: %v", err)
- }
- if resp.Recurrence != "Repeats weekly on Monday" {
- t.Errorf("recurrence = %q, want %q", resp.Recurrence, "Repeats weekly on Monday")
- }
-}
-
-func TestHandleWidgetRecurrence_NotFound_Returns404(t *testing.T) {
- mock := &MockCalendarClient{RecurrenceErr: fmt.Errorf("not found")}
- h := &Handler{googleCalendarClient: mock}
- req := httptest.NewRequest("GET", "/api/widget/recurrence?recurring_event_id=missing", nil)
- w := httptest.NewRecorder()
- http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Fatalf("expected 404, got %d", w.Code)
- }
-}
-
-func TestHandleWidgetRecurrence_MissingParam_Returns400(t *testing.T) {
- h := &Handler{}
- req := httptest.NewRequest("GET", "/api/widget/recurrence", nil)
- w := httptest.NewRecorder()
- http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Fatalf("expected 400, got %d", w.Code)
- }
-}