diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-13 17:52:10 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-16 02:42:31 +0000 |
| commit | c4066f78d83b1440155248f9bc724b905aa88697 (patch) | |
| tree | 70b557a1e340f12aa125fd0c53effabcbda4027b | |
| parent | fb41a5eb47a4213c3f9a7095da4b8741f1598725 (diff) | |
feat(widget): tap event to open its source directly, drop detail popup
Tapping a calendar event (or any event-type item) now opens the event's
URL (Google Calendar, Plan to Eat, etc.) directly instead of showing an
intermediate popup with an "Open in Calendar" button. Removes
EventDetailActivity and the recurrence-schedule lookup it was the only
consumer of: WidgetRepository.getRecurrence, the Go
/api/widget/recurrence endpoint, HandleWidgetRecurrence,
GoogleCalendarAPI.GetRecurrenceRule, and formatRecurrence, plus their
tests. RecurringEventID itself stays -- it's general calendar-sync
metadata used elsewhere in the timeline pipeline, not exclusive to the
removed popup.
| -rw-r--r-- | android/app/src/main/AndroidManifest.xml | 7 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt | 20 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt | 37 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt | 145 | ||||
| -rw-r--r-- | cmd/dashboard/main.go | 1 | ||||
| -rw-r--r-- | internal/api/google_calendar.go | 98 | ||||
| -rw-r--r-- | internal/api/google_calendar_test.go | 30 | ||||
| -rw-r--r-- | internal/api/interfaces.go | 1 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic_test.go | 7 | ||||
| -rw-r--r-- | internal/handlers/widget.go | 22 | ||||
| -rw-r--r-- | internal/handlers/widget_test.go | 47 |
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) - } -} |
