feat(slides): add create-from-template command with text replacement
Adds `gog slides create-from-template` command to create presentations
from templates with automatic placeholder text replacement.
Features:
- {{key}} placeholder format (auto-wrapped)
- Multiple replacement sources: --replace flags and --replacements JSON file
- Type conversion for JSON (numbers, booleans → strings)
- --exact flag for arbitrary string replacement
- Replacement statistics in output
- JSON/text output modes
Implementation:
- Copy template via Drive API
- Batch text replacement via Slides API ReplaceAllText
- Comprehensive error handling and validation
- 7 test cases covering all features and edge cases
Usage: gog slides create-from-template <templateId> <title> \
--replace "key=value" \
--replacements data.json
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ffd8cd482b
commit
922c1fbb8b
@ -16,6 +16,7 @@
|
||||
- Gmail: add `gmail labels rename` to rename user labels by ID or exact name, with system-label guards and wrong-case ID safety. (#391) — thanks @adam-zethraeus.
|
||||
- Gmail: add `gmail messages modify` for single-message label changes, complementing thread- and batch-level modify flows. (#281) — thanks @zerone0x.
|
||||
- Calendar: add `calendar subscribe` (aliases `sub`, `add-calendar`) to add a shared calendar to the current account’s calendar list. (#327) — thanks @cdthompson.
|
||||
- Slides: add `create-from-template` with `--replace` / `--replacements`, dry-run support, and template placeholder replacement stats. (#273) — thanks @penguinco.
|
||||
- Sheets: add `sheets update-note` / `set-note` to write or clear cell notes across a range. (#430) — thanks @andybergon.
|
||||
- Sheets: add `sheets create --parent` to place new spreadsheets in a Drive folder. (#424) — thanks @ManManavadaria.
|
||||
- Contacts: support `--org`, `--title`, `--url`, `--note`, and `--custom` on create/update; include custom fields in get output with deterministic ordering. (#199) — thanks @phuctm97.
|
||||
|
||||
23
README.md
23
README.md
@ -18,7 +18,7 @@ Fast, script-friendly CLI for Gmail, Calendar, Chat, Classroom, Drive, Docs, Sli
|
||||
- **Sheets** - read/write/update spreadsheets, insert rows/cols, format cells, read notes, create new sheets (and export via Drive)
|
||||
- **Forms** - create/get forms and inspect responses
|
||||
- **Apps Script** - create/get projects, inspect content, and run functions
|
||||
- **Docs/Slides** - export to PDF/DOCX/PPTX via Drive (plus create/copy, docs-to-text, and **sedmat** sed-style document editing with Markdown formatting, images, and tables)
|
||||
- **Docs/Slides** - export to PDF/DOCX/PPTX via Drive (plus create/copy, docs-to-text, template-based creation with text replacement, and **sedmat** sed-style document editing with Markdown formatting, images, and tables)
|
||||
- **People** - access profile information
|
||||
- **Keep (Workspace only)** - list/get/search notes and download attachments (service account + domain-wide delegation)
|
||||
- **Groups** - list groups you belong to, view group members (Google Workspace)
|
||||
@ -916,6 +916,7 @@ gog docs find-replace <docId> "old" "new" --tab-id t.notes
|
||||
gog slides info <presentationId>
|
||||
gog slides create "My Deck"
|
||||
gog slides create-from-markdown "My Deck" --content-file ./slides.md
|
||||
gog slides create-from-template <templateId> "My Deck" --replace "name=John" --replace "date=2026-02-15"
|
||||
gog slides copy <presentationId> "My Deck Copy"
|
||||
gog slides export <presentationId> --format pdf --out ./deck.pdf
|
||||
gog slides list-slides <presentationId>
|
||||
@ -1263,6 +1264,26 @@ gog docs sed <docId> 's/|1|[col:$+]//' # append column at end
|
||||
# Export (via Drive)
|
||||
gog slides export <presentationId> --format pptx --out ./deck.pptx
|
||||
gog slides export <presentationId> --format pdf --out ./deck.pdf
|
||||
|
||||
# Create from template with text replacements
|
||||
gog slides create-from-template <templateId> "Q1 Report" \
|
||||
--replace "quarter=Q1 2026" \
|
||||
--replace "revenue=$1.2M" \
|
||||
--replace "growth=15%"
|
||||
|
||||
# Use JSON file for many replacements
|
||||
cat > replacements.json <<EOF
|
||||
{
|
||||
"name": "John Doe",
|
||||
"title": "Sales Manager",
|
||||
"date": "2026-02-15",
|
||||
"sales": "125",
|
||||
"target": "100"
|
||||
}
|
||||
EOF
|
||||
|
||||
gog slides create-from-template <templateId> "Monthly Report" \
|
||||
--replacements replacements.json
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
226
docs/slides-template-replacement.md
Normal file
226
docs/slides-template-replacement.md
Normal file
@ -0,0 +1,226 @@
|
||||
# Google Slides Template Text Replacement
|
||||
|
||||
## Overview
|
||||
|
||||
The `gog slides create-from-template` command allows you to create a new Google Slides presentation from a template and automatically replace placeholder text throughout the presentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
gog slides create-from-template <templateId> <title> \
|
||||
--replace "key=value" \
|
||||
--replace "another=value"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Placeholder Format
|
||||
|
||||
By default, the command looks for placeholders in the format `{{key}}` in your template:
|
||||
|
||||
- In your template, add text like: `{{name}}`, `{{date}}`, `{{company}}`
|
||||
- The command automatically wraps keys with `{{}}` if not already present
|
||||
- Use `--exact` flag to match exact strings without `{{}}` wrapping
|
||||
|
||||
### 2. Multiple Replacement Sources
|
||||
|
||||
#### Command-line flags (for a few replacements):
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123 "Q1 Report" \
|
||||
--replace "quarter=Q1 2026" \
|
||||
--replace "revenue=$1.2M" \
|
||||
--replace "growth=15%"
|
||||
```
|
||||
|
||||
#### JSON file (for many replacements):
|
||||
|
||||
Create a JSON file with your replacements:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "John Doe",
|
||||
"title": "Sales Manager",
|
||||
"date": "2026-02-15",
|
||||
"sales": 125,
|
||||
"target": 100,
|
||||
"achieved": true
|
||||
}
|
||||
```
|
||||
|
||||
Then use it:
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123 "Monthly Report" \
|
||||
--replacements replacements.json
|
||||
```
|
||||
|
||||
#### Combining both (flags override file):
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123 "Report" \
|
||||
--replacements base-data.json \
|
||||
--replace "date=2026-02-15" # This overrides "date" from JSON
|
||||
```
|
||||
|
||||
### 3. Type Conversion
|
||||
|
||||
When using JSON files, non-string values are automatically converted:
|
||||
- Numbers: `125` → `"125"`
|
||||
- Booleans: `true` → `"true"`
|
||||
- Null: `null` → `""`
|
||||
- Complex types: JSON-encoded
|
||||
|
||||
### 4. Exact String Matching
|
||||
|
||||
Use `--exact` to replace arbitrary text without `{{}}` wrapping:
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123 "Report" \
|
||||
--replace "OLD_TEXT=NEW_TEXT" \
|
||||
--exact
|
||||
```
|
||||
|
||||
This is useful for templates not using the `{{key}}` convention.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Report Generation
|
||||
|
||||
Template contains:
|
||||
- `{{employee_name}}`
|
||||
- `{{report_date}}`
|
||||
- `{{sales_total}}`
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123def456 "January Sales Report" \
|
||||
--replace "employee_name=Jane Smith" \
|
||||
--replace "report_date=2026-01-31" \
|
||||
--replace "sales_total=$45,000"
|
||||
```
|
||||
|
||||
### Example 2: Batch Report Generation
|
||||
|
||||
Create `report-data.json`:
|
||||
```json
|
||||
{
|
||||
"month": "February",
|
||||
"year": "2026",
|
||||
"sales": "$52,000",
|
||||
"target": "$50,000",
|
||||
"performance": "104%"
|
||||
}
|
||||
```
|
||||
|
||||
Generate report:
|
||||
```bash
|
||||
gog slides create-from-template 1abc123def456 "February Sales Report" \
|
||||
--replacements report-data.json \
|
||||
--parent 1xyz789abc123 # Optional: place in specific folder
|
||||
```
|
||||
|
||||
### Example 3: Scripted Bulk Generation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
TEMPLATE_ID="1abc123def456"
|
||||
|
||||
# Read CSV and generate presentations
|
||||
while IFS=, read -r name title date sales; do
|
||||
gog slides create-from-template "$TEMPLATE_ID" "Report - $name" \
|
||||
--replace "name=$name" \
|
||||
--replace "title=$title" \
|
||||
--replace "date=$date" \
|
||||
--replace "sales=$sales" \
|
||||
--json > "output-$name.json"
|
||||
done < employees.csv
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
### Text Output
|
||||
|
||||
```
|
||||
Created presentation from template
|
||||
id 1new456presentation
|
||||
name Q1 Report
|
||||
link https://docs.google.com/presentation/d/1new456presentation/edit
|
||||
|
||||
Replacements:
|
||||
quarter 3 occurrences
|
||||
revenue 2 occurrences
|
||||
growth 1 occurrences
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
```bash
|
||||
gog slides create-from-template 1abc123 "Report" \
|
||||
--replace "name=John" \
|
||||
--json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"presentationId": "1new456presentation",
|
||||
"name": "Report",
|
||||
"link": "https://docs.google.com/presentation/d/1new456presentation/edit",
|
||||
"replacements": {
|
||||
"name": 3,
|
||||
"date": 2,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Test your template first**: Create a test presentation to verify all placeholders are correctly placed
|
||||
2. **Use consistent naming**: Stick to `{{key}}` format for clarity
|
||||
3. **Check replacement counts**: The output shows how many times each placeholder was found
|
||||
4. **Use JSON for complex data**: Easier to manage many fields or computed values
|
||||
5. **Save folder IDs**: Use `--parent` to organize generated presentations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Placeholder not found (0 occurrences)
|
||||
|
||||
- Verify the placeholder exists in the template
|
||||
- Check for typos in placeholder names (case-sensitive by default)
|
||||
- Ensure `{{}}` wrapping is correct (or use `--exact`)
|
||||
|
||||
### Permission denied
|
||||
|
||||
- Ensure you have edit access to the template
|
||||
- Check that the Google Slides API is enabled for your OAuth client
|
||||
|
||||
### Template copy failed
|
||||
|
||||
- Verify the template ID is correct
|
||||
- Check that the template is accessible with your account
|
||||
- Ensure it's a Google Slides presentation (not a document or sheet)
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Preserve some placeholders
|
||||
|
||||
To replace only some placeholders and keep others for later editing:
|
||||
|
||||
```bash
|
||||
# Only replace quarter and keep other {{}} placeholders
|
||||
gog slides create-from-template 1abc123 "Draft Report" \
|
||||
--replace "quarter=Q1 2026"
|
||||
```
|
||||
|
||||
### Case-sensitive vs case-insensitive
|
||||
|
||||
By default, replacements are case-sensitive. All placeholders and keys must match exactly.
|
||||
|
||||
### Whitespace in values
|
||||
|
||||
Values are not trimmed, allowing intentional whitespace:
|
||||
|
||||
```bash
|
||||
--replace "description= Indented text"
|
||||
```
|
||||
@ -24,6 +24,7 @@ type SlidesCmd struct {
|
||||
Info SlidesInfoCmd `cmd:"" name:"info" aliases:"get,show" help:"Get Google Slides presentation metadata"`
|
||||
Create SlidesCreateCmd `cmd:"" name:"create" aliases:"add,new" help:"Create a Google Slides presentation"`
|
||||
CreateFromMarkdown SlidesCreateFromMarkdownCmd `cmd:"" name:"create-from-markdown" help:"Create a Google Slides presentation from markdown"`
|
||||
CreateFromTemplate SlidesCreateFromTemplateCmd `cmd:"" name:"create-from-template" help:"Create a presentation from template with text replacements"`
|
||||
Copy SlidesCopyCmd `cmd:"" name:"copy" aliases:"cp,duplicate" help:"Copy a Google Slides presentation"`
|
||||
AddSlide SlidesAddSlideCmd `cmd:"" name:"add-slide" help:"Add a slide with a full-bleed image and optional speaker notes"`
|
||||
ListSlides SlidesListSlidesCmd `cmd:"" name:"list-slides" help:"List all slides with their object IDs"`
|
||||
|
||||
250
internal/cmd/slides_create_from_template.go
Normal file
250
internal/cmd/slides_create_from_template.go
Normal file
@ -0,0 +1,250 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/drive/v3"
|
||||
"google.golang.org/api/slides/v1"
|
||||
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
type SlidesCreateFromTemplateCmd struct {
|
||||
TemplateID string `arg:"" name:"templateId" help:"Template presentation ID"`
|
||||
Title string `arg:"" name:"title" help:"New presentation title"`
|
||||
Replace []string `name:"replace" help:"Text replacement in format 'key=value' (repeatable)"`
|
||||
Replacements string `name:"replacements" help:"JSON file containing replacements" type:"existingfile"`
|
||||
Parent string `name:"parent" help:"Destination folder ID"`
|
||||
Exact bool `name:"exact" help:"Use exact string matching instead of {{key}} placeholders"`
|
||||
}
|
||||
|
||||
func (c *SlidesCreateFromTemplateCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
u := ui.FromContext(ctx)
|
||||
account, err := requireAccount(flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateID := normalizeGoogleID(strings.TrimSpace(c.TemplateID))
|
||||
if templateID == "" {
|
||||
return usage("empty templateId")
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(c.Title)
|
||||
if title == "" {
|
||||
return usage("empty title")
|
||||
}
|
||||
|
||||
// Parse replacements from both sources
|
||||
replacements, err := c.parseReplacements()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(replacements) == 0 {
|
||||
return usage("no replacements specified (use --replace or --replacements)")
|
||||
}
|
||||
|
||||
parent := normalizeGoogleID(strings.TrimSpace(c.Parent))
|
||||
if dryRunErr := dryRunExit(ctx, flags, "slides.create-from-template", map[string]any{
|
||||
"template_id": templateID,
|
||||
"title": title,
|
||||
"parent": parent,
|
||||
"exact": c.Exact,
|
||||
"replacements": replacements,
|
||||
}); dryRunErr != nil {
|
||||
return dryRunErr
|
||||
}
|
||||
|
||||
// Create Drive service to copy the template
|
||||
driveSvc, err := newDriveService(ctx, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy template
|
||||
f := &drive.File{
|
||||
Name: title,
|
||||
}
|
||||
if parent != "" {
|
||||
f.Parents = []string{parent}
|
||||
}
|
||||
|
||||
created, err := driveSvc.Files.Copy(templateID, f).
|
||||
SupportsAllDrives(true).
|
||||
Fields("id, name, mimeType, webViewLink").
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy template: %w", err)
|
||||
}
|
||||
|
||||
if created == nil {
|
||||
return errors.New("template copy failed")
|
||||
}
|
||||
|
||||
// Verify it's a presentation
|
||||
if created.MimeType != "application/vnd.google-apps.presentation" {
|
||||
return fmt.Errorf("template is not a Google Slides presentation (got %s)", created.MimeType)
|
||||
}
|
||||
|
||||
presentationID := created.Id
|
||||
|
||||
// Create Slides service for text replacement
|
||||
slidesSvc, err := newSlidesService(ctx, account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create slides service: %w", err)
|
||||
}
|
||||
|
||||
// Build batch update requests for text replacement
|
||||
requests := make([]*slides.Request, 0, len(replacements))
|
||||
for key, value := range replacements {
|
||||
searchText := key
|
||||
if !c.Exact {
|
||||
// Wrap with {{}} if not already present
|
||||
if !strings.HasPrefix(key, "{{") || !strings.HasSuffix(key, "}}") {
|
||||
searchText = fmt.Sprintf("{{%s}}", key)
|
||||
}
|
||||
}
|
||||
|
||||
requests = append(requests, &slides.Request{
|
||||
ReplaceAllText: &slides.ReplaceAllTextRequest{
|
||||
ContainsText: &slides.SubstringMatchCriteria{
|
||||
Text: searchText,
|
||||
MatchCase: true,
|
||||
},
|
||||
ReplaceText: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Execute batch update
|
||||
result, err := slidesSvc.Presentations.BatchUpdate(presentationID, &slides.BatchUpdatePresentationRequest{
|
||||
Requests: requests,
|
||||
}).Context(ctx).Do()
|
||||
if err != nil {
|
||||
u.Err().Printf("Warning: presentation created but text replacement failed: %v", err)
|
||||
u.Err().Printf("Presentation ID: %s", presentationID)
|
||||
u.Err().Printf("You may need to manually edit or delete this presentation")
|
||||
return fmt.Errorf("text replacement failed: %w", err)
|
||||
}
|
||||
|
||||
// Collect replacement statistics
|
||||
replacementStats := make(map[string]int64)
|
||||
for i, reply := range result.Replies {
|
||||
if reply.ReplaceAllText != nil {
|
||||
// Get the original key from requests
|
||||
if i < len(requests) && requests[i].ReplaceAllText != nil {
|
||||
searchText := requests[i].ReplaceAllText.ContainsText.Text
|
||||
// Remove {{}} for display if present
|
||||
displayKey := strings.TrimSuffix(strings.TrimPrefix(searchText, "{{"), "}}")
|
||||
replacementStats[displayKey] = reply.ReplaceAllText.OccurrencesChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output results
|
||||
if outfmt.IsJSON(ctx) {
|
||||
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
|
||||
"presentationId": presentationID,
|
||||
"name": created.Name,
|
||||
"link": created.WebViewLink,
|
||||
"replacements": replacementStats,
|
||||
})
|
||||
}
|
||||
|
||||
u.Out().Printf("Created presentation from template")
|
||||
u.Out().Printf("id\t%s", presentationID)
|
||||
u.Out().Printf("name\t%s", created.Name)
|
||||
if created.WebViewLink != "" {
|
||||
u.Out().Printf("link\t%s", created.WebViewLink)
|
||||
}
|
||||
|
||||
if len(replacementStats) > 0 {
|
||||
u.Out().Println("")
|
||||
u.Out().Println("Replacements:")
|
||||
for key, count := range replacementStats {
|
||||
if count > 0 {
|
||||
u.Out().Printf(" %s\t%d occurrences", key, count)
|
||||
} else {
|
||||
u.Out().Printf(" %s\tnot found", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseReplacements combines replacements from --replace flags and --replacements file
|
||||
func (c *SlidesCreateFromTemplateCmd) parseReplacements() (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// Load from JSON file first
|
||||
if c.Replacements != "" {
|
||||
data, err := os.ReadFile(c.Replacements)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read replacements file: %w", err)
|
||||
}
|
||||
|
||||
var fileReplacements map[string]interface{}
|
||||
if err := json.Unmarshal(data, &fileReplacements); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON in replacements file: %w", err)
|
||||
}
|
||||
|
||||
// Convert all values to strings
|
||||
for k, v := range fileReplacements {
|
||||
k = strings.TrimSpace(k)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
result[k] = val
|
||||
case float64:
|
||||
result[k] = fmt.Sprintf("%g", val)
|
||||
case bool:
|
||||
result[k] = fmt.Sprintf("%t", val)
|
||||
case nil:
|
||||
result[k] = ""
|
||||
default:
|
||||
// Try to marshal back to JSON for complex types
|
||||
jsonVal, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot convert value for key %q to string: %w", k, err)
|
||||
}
|
||||
result[k] = string(jsonVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process --replace flags (these override file values)
|
||||
for _, replacement := range c.Replace {
|
||||
parts := strings.SplitN(replacement, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid replacement format %q (expected key=value)", replacement)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := parts[1] // Don't trim value - it might be intentionally whitespace
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("empty key in replacement %q", replacement)
|
||||
}
|
||||
|
||||
// Warn if using non-placeholder key without --exact flag
|
||||
if !c.Exact && !strings.HasPrefix(key, "{{") && !strings.HasSuffix(key, "}}") {
|
||||
// This is OK, we'll add {{}} automatically
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
637
internal/cmd/slides_create_from_template_test.go
Normal file
637
internal/cmd/slides_create_from_template_test.go
Normal file
@ -0,0 +1,637 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/api/drive/v3"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/slides/v1"
|
||||
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
func TestSlidesCreateFromTemplate_Basic(t *testing.T) {
|
||||
var capturedDriveRequest *http.Request
|
||||
var capturedSlidesRequests []*slides.Request
|
||||
|
||||
driveServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedDriveRequest = r
|
||||
|
||||
// Handle copy request - the path includes /v3/files/{id}/copy
|
||||
if r.Method == "POST" && strings.Contains(r.URL.Path, "/files/template123/copy") {
|
||||
response := &drive.File{
|
||||
Id: "copied123",
|
||||
Name: "New Presentation",
|
||||
MimeType: "application/vnd.google-apps.presentation",
|
||||
WebViewLink: "https://docs.google.com/presentation/d/copied123/edit",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer driveServer.Close()
|
||||
|
||||
slidesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && r.URL.Path == "/v1/presentations/copied123:batchUpdate" {
|
||||
var req slides.BatchUpdatePresentationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
capturedSlidesRequests = req.Requests
|
||||
|
||||
// Build response with replacement statistics
|
||||
replies := make([]*slides.Response, len(req.Requests))
|
||||
for i := range req.Requests {
|
||||
replies[i] = &slides.Response{
|
||||
ReplaceAllText: &slides.ReplaceAllTextResponse{
|
||||
OccurrencesChanged: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response := &slides.BatchUpdatePresentationResponse{
|
||||
PresentationId: "copied123",
|
||||
Replies: replies,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer slidesServer.Close()
|
||||
|
||||
// Create Drive service
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(driveServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create Slides service
|
||||
slidesSvc, err := slides.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(slidesServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldNewDrive := newDriveService
|
||||
oldNewSlides := newSlidesService
|
||||
defer func() {
|
||||
newDriveService = oldNewDrive
|
||||
newSlidesService = oldNewSlides
|
||||
}()
|
||||
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newSlidesService = func(context.Context, string) (*slides.Service, error) { return slidesSvc, nil }
|
||||
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template123",
|
||||
Title: "New Presentation",
|
||||
Replace: []string{"name=John Doe", "company=ACME Corp"},
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err = cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("Run failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify Drive API call
|
||||
if capturedDriveRequest == nil {
|
||||
t.Fatal("Drive API was not called")
|
||||
}
|
||||
|
||||
// Verify Slides API calls
|
||||
if len(capturedSlidesRequests) != 2 {
|
||||
t.Fatalf("Expected 2 replacement requests, got %d", len(capturedSlidesRequests))
|
||||
}
|
||||
|
||||
// Check first replacement
|
||||
if capturedSlidesRequests[0].ReplaceAllText == nil {
|
||||
t.Fatal("First request is not ReplaceAllText")
|
||||
}
|
||||
if capturedSlidesRequests[0].ReplaceAllText.ContainsText.Text != "{{name}}" {
|
||||
t.Errorf("Expected {{name}}, got %s", capturedSlidesRequests[0].ReplaceAllText.ContainsText.Text)
|
||||
}
|
||||
if capturedSlidesRequests[0].ReplaceAllText.ReplaceText != "John Doe" {
|
||||
t.Errorf("Expected 'John Doe', got %s", capturedSlidesRequests[0].ReplaceAllText.ReplaceText)
|
||||
}
|
||||
|
||||
// Check second replacement
|
||||
if capturedSlidesRequests[1].ReplaceAllText == nil {
|
||||
t.Fatal("Second request is not ReplaceAllText")
|
||||
}
|
||||
if capturedSlidesRequests[1].ReplaceAllText.ContainsText.Text != "{{company}}" {
|
||||
t.Errorf("Expected {{company}}, got %s", capturedSlidesRequests[1].ReplaceAllText.ContainsText.Text)
|
||||
}
|
||||
if capturedSlidesRequests[1].ReplaceAllText.ReplaceText != "ACME Corp" {
|
||||
t.Errorf("Expected 'ACME Corp', got %s", capturedSlidesRequests[1].ReplaceAllText.ReplaceText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_JSONFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "replacements.json")
|
||||
|
||||
replacements := map[string]interface{}{
|
||||
"name": "Jane Smith",
|
||||
"age": 30,
|
||||
"active": true,
|
||||
"company": "TechCorp",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(replacements)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(jsonFile, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var capturedSlidesRequests []*slides.Request
|
||||
|
||||
driveServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && strings.Contains(r.URL.Path, "/files/template456/copy") {
|
||||
response := &drive.File{
|
||||
Id: "copied456",
|
||||
Name: "Test Presentation",
|
||||
MimeType: "application/vnd.google-apps.presentation",
|
||||
WebViewLink: "https://docs.google.com/presentation/d/copied456/edit",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer driveServer.Close()
|
||||
|
||||
slidesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && r.URL.Path == "/v1/presentations/copied456:batchUpdate" {
|
||||
var req slides.BatchUpdatePresentationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
capturedSlidesRequests = req.Requests
|
||||
|
||||
replies := make([]*slides.Response, len(req.Requests))
|
||||
for i := range req.Requests {
|
||||
replies[i] = &slides.Response{
|
||||
ReplaceAllText: &slides.ReplaceAllTextResponse{
|
||||
OccurrencesChanged: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response := &slides.BatchUpdatePresentationResponse{
|
||||
PresentationId: "copied456",
|
||||
Replies: replies,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer slidesServer.Close()
|
||||
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(driveServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
slidesSvc, err := slides.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(slidesServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldNewDrive := newDriveService
|
||||
oldNewSlides := newSlidesService
|
||||
defer func() {
|
||||
newDriveService = oldNewDrive
|
||||
newSlidesService = oldNewSlides
|
||||
}()
|
||||
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newSlidesService = func(context.Context, string) (*slides.Service, error) { return slidesSvc, nil }
|
||||
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template456",
|
||||
Title: "Test Presentation",
|
||||
Replacements: jsonFile,
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err = cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("Run failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have 4 replacements
|
||||
if len(capturedSlidesRequests) != 4 {
|
||||
t.Fatalf("Expected 4 replacement requests, got %d", len(capturedSlidesRequests))
|
||||
}
|
||||
|
||||
// Verify type conversions
|
||||
foundAge := false
|
||||
foundActive := false
|
||||
for _, req := range capturedSlidesRequests {
|
||||
if req.ReplaceAllText != nil {
|
||||
text := req.ReplaceAllText.ContainsText.Text
|
||||
if text == "{{age}}" {
|
||||
foundAge = true
|
||||
if req.ReplaceAllText.ReplaceText != "30" {
|
||||
t.Errorf("Expected age '30', got %s", req.ReplaceAllText.ReplaceText)
|
||||
}
|
||||
}
|
||||
if text == "{{active}}" {
|
||||
foundActive = true
|
||||
if req.ReplaceAllText.ReplaceText != "true" {
|
||||
t.Errorf("Expected active 'true', got %s", req.ReplaceAllText.ReplaceText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundAge {
|
||||
t.Error("Did not find age replacement")
|
||||
}
|
||||
if !foundActive {
|
||||
t.Error("Did not find active replacement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_ExactMode(t *testing.T) {
|
||||
var capturedSlidesRequests []*slides.Request
|
||||
|
||||
driveServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && strings.Contains(r.URL.Path, "/files/template789/copy") {
|
||||
response := &drive.File{
|
||||
Id: "copied789",
|
||||
Name: "Exact Mode Test",
|
||||
MimeType: "application/vnd.google-apps.presentation",
|
||||
WebViewLink: "https://docs.google.com/presentation/d/copied789/edit",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer driveServer.Close()
|
||||
|
||||
slidesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" && r.URL.Path == "/v1/presentations/copied789:batchUpdate" {
|
||||
var req slides.BatchUpdatePresentationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
capturedSlidesRequests = req.Requests
|
||||
|
||||
replies := make([]*slides.Response, len(req.Requests))
|
||||
for i := range req.Requests {
|
||||
replies[i] = &slides.Response{
|
||||
ReplaceAllText: &slides.ReplaceAllTextResponse{
|
||||
OccurrencesChanged: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response := &slides.BatchUpdatePresentationResponse{
|
||||
PresentationId: "copied789",
|
||||
Replies: replies,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer slidesServer.Close()
|
||||
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(driveServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
slidesSvc, err := slides.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(slidesServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldNewDrive := newDriveService
|
||||
oldNewSlides := newSlidesService
|
||||
defer func() {
|
||||
newDriveService = oldNewDrive
|
||||
newSlidesService = oldNewSlides
|
||||
}()
|
||||
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newSlidesService = func(context.Context, string) (*slides.Service, error) { return slidesSvc, nil }
|
||||
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template789",
|
||||
Title: "Exact Mode Test",
|
||||
Replace: []string{"OLD_TEXT=NEW_TEXT"},
|
||||
Exact: true,
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err = cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("Run failed: %v", err)
|
||||
}
|
||||
|
||||
if len(capturedSlidesRequests) != 1 {
|
||||
t.Fatalf("Expected 1 replacement request, got %d", len(capturedSlidesRequests))
|
||||
}
|
||||
|
||||
// In exact mode, should search for "OLD_TEXT" not "{{OLD_TEXT}}"
|
||||
if capturedSlidesRequests[0].ReplaceAllText.ContainsText.Text != "OLD_TEXT" {
|
||||
t.Errorf("Expected 'OLD_TEXT', got %s", capturedSlidesRequests[0].ReplaceAllText.ContainsText.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_EmptyReplacements(t *testing.T) {
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template123",
|
||||
Title: "Test",
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err := cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for empty replacements, got nil")
|
||||
}
|
||||
if ExitCode(err) != 2 {
|
||||
t.Errorf("Expected usage error (exit code 2), got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_InvalidReplaceFormat(t *testing.T) {
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template123",
|
||||
Title: "Test",
|
||||
Replace: []string{"invalid_no_equals_sign"},
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err := cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid replace format, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "invalid.json")
|
||||
|
||||
if err := os.WriteFile(jsonFile, []byte("{invalid json}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template123",
|
||||
Title: "Test",
|
||||
Replacements: jsonFile,
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err := cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid JSON, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_CombineFileAndFlags(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonFile := filepath.Join(tmpDir, "replacements.json")
|
||||
|
||||
fileReplacements := map[string]string{
|
||||
"name": "From File",
|
||||
"company": "File Corp",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(fileReplacements)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(jsonFile, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var capturedSlidesRequests []*slides.Request
|
||||
|
||||
driveServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
response := &drive.File{
|
||||
Id: "copied999",
|
||||
Name: "Combined Test",
|
||||
MimeType: "application/vnd.google-apps.presentation",
|
||||
WebViewLink: "https://docs.google.com/presentation/d/copied999/edit",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer driveServer.Close()
|
||||
|
||||
slidesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
var req slides.BatchUpdatePresentationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
capturedSlidesRequests = req.Requests
|
||||
|
||||
replies := make([]*slides.Response, len(req.Requests))
|
||||
for i := range req.Requests {
|
||||
replies[i] = &slides.Response{
|
||||
ReplaceAllText: &slides.ReplaceAllTextResponse{
|
||||
OccurrencesChanged: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response := &slides.BatchUpdatePresentationResponse{
|
||||
PresentationId: "copied999",
|
||||
Replies: replies,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer slidesServer.Close()
|
||||
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(driveServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
slidesSvc, err := slides.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithEndpoint(slidesServer.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldNewDrive := newDriveService
|
||||
oldNewSlides := newSlidesService
|
||||
defer func() {
|
||||
newDriveService = oldNewDrive
|
||||
newSlidesService = oldNewSlides
|
||||
}()
|
||||
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newSlidesService = func(context.Context, string) (*slides.Service, error) { return slidesSvc, nil }
|
||||
|
||||
// Flag overrides file
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template999",
|
||||
Title: "Combined Test",
|
||||
Replacements: jsonFile,
|
||||
Replace: []string{"name=From Flag"},
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err = cmd.Run(ctx, &RootFlags{Account: "test@example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("Run failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have 2 replacements (name and company)
|
||||
if len(capturedSlidesRequests) != 2 {
|
||||
t.Fatalf("Expected 2 replacement requests, got %d", len(capturedSlidesRequests))
|
||||
}
|
||||
|
||||
// Verify that flag value overrides file value
|
||||
foundNameOverride := false
|
||||
for _, req := range capturedSlidesRequests {
|
||||
if req.ReplaceAllText != nil && req.ReplaceAllText.ContainsText.Text == "{{name}}" {
|
||||
foundNameOverride = true
|
||||
if req.ReplaceAllText.ReplaceText != "From Flag" {
|
||||
t.Errorf("Expected 'From Flag', got %s", req.ReplaceAllText.ReplaceText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundNameOverride {
|
||||
t.Error("Flag should override file value for 'name'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateFromTemplate_DryRunSkipsAPICalls(t *testing.T) {
|
||||
origNewDrive := newDriveService
|
||||
origNewSlides := newSlidesService
|
||||
t.Cleanup(func() {
|
||||
newDriveService = origNewDrive
|
||||
newSlidesService = origNewSlides
|
||||
})
|
||||
|
||||
driveCalls := 0
|
||||
slidesCalls := 0
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) {
|
||||
driveCalls++
|
||||
t.Fatal("drive service should not be created during dry-run")
|
||||
return nil, nil
|
||||
}
|
||||
newSlidesService = func(context.Context, string) (*slides.Service, error) {
|
||||
slidesCalls++
|
||||
t.Fatal("slides service should not be created during dry-run")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd := &SlidesCreateFromTemplateCmd{
|
||||
TemplateID: "template123",
|
||||
Title: "Dry Run Deck",
|
||||
Replace: []string{"name=John Doe"},
|
||||
Parent: "https://drive.google.com/drive/folders/parent123",
|
||||
}
|
||||
|
||||
u, uiErr := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr, Color: "never"})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
|
||||
err := cmd.Run(ctx, &RootFlags{Account: "test@example.com", DryRun: true, NoInput: true})
|
||||
if ExitCode(err) != 0 {
|
||||
t.Fatalf("expected dry-run exit 0, got %v", err)
|
||||
}
|
||||
if driveCalls != 0 || slidesCalls != 0 {
|
||||
t.Fatalf("expected no API calls, got drive=%d slides=%d", driveCalls, slidesCalls)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user