package gsheet2csv import ( "errors" "io" "net/http" "slices" "strings" "testing" ) // mockHTTPClient allows controlling HTTP responses for testing. type mockHTTPClient struct { resp *http.Response err error } func (m *mockHTTPClient) Get(url string) (*http.Response, error) { return m.resp, m.err } // sampleCSV mimics the structure of ai-models.csv from the project README. const sampleCSV = `# Generated by ollama list "# Sample Quoted Comment, with ""quotes"" itself" "NAME","ID","SIZE","MODIFIED" "qwen3-coder:30b","06c1097efce0","18 GB","8 days ago" "gpt-oss:20b","aa4295ac10c3","13 GB","8 days ago" "gpt-oss:latest","aa4295ac10c3","13 GB","7 weeks ago" ` // malformedCSV for testing error handling. const malformedCSV = `# Comment "NAME","ID","SIZE","MODIFIED "qwen3-coder:30b","06c1097efce0","18 GB","8 days ago" ` // TestParseIDs verifies the ParseIDs function for various URL formats. func TestParseIDs(t *testing.T) { tests := []struct { name string url string wantDoc string wantGid string }{ { name: "Google Sheets Edit / Share URL with gid", url: "https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/edit?gid=559037238#gid=559037238", wantDoc: "1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34", wantGid: "559037238", }, { name: "Google Sheets CSV URL with gid", url: "https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/export?format=csv&usp=sharing&gid=559037238", wantDoc: "1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34", wantGid: "559037238", }, { name: "URL without gid", url: "https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/edit", wantDoc: "1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34", wantGid: "0", }, { name: "Invalid URL", url: "https://example.com/invalid", wantDoc: "", wantGid: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotDoc, gotGid := ParseIDs(tt.url) if gotDoc != tt.wantDoc { t.Errorf("ParseIDs() docid = %q, want %q", gotDoc, tt.wantDoc) } if gotGid != tt.wantGid { t.Errorf("ParseIDs() gid = %q, want %q", gotGid, tt.wantGid) } }) } } // TestNewReaderFromURL tests initializing a Reader from a Google Sheets URL. func TestNewReaderFromURL(t *testing.T) { originalGet := httpGet defer func() { httpGet = originalGet }() url := "https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/edit?gid=559037238" // Test successful HTTP response mockResp := &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(sampleCSV)), } client := &mockHTTPClient{resp: mockResp} httpGet = client.Get reader := NewReaderFromURL(url) if reader.err != nil { t.Errorf("NewReaderFromURL() unexpected error: %v", reader.err) } if reader.resp != mockResp { t.Error("NewReaderFromURL() did not set response correctly") } if !reader.close { t.Error("NewReaderFromURL() did not set close flag") } // Test HTTP failure client = &mockHTTPClient{resp: mockResp} client.err = errors.New("network error") httpGet = client.Get reader = NewReaderFromURL(url) if reader.err == nil { t.Error("NewReaderFromURL() expected error, got nil") } // Test non-200 status client = &mockHTTPClient{resp: &http.Response{ StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader("these aren't the droids you're looking for")), }} httpGet = client.Get reader = NewReaderFromURL(url) if reader.err == nil { t.Error("NewReaderFromURL() expected error for non-200 status, got nil") } } // TestRead tests the Read method for comment handling. func TestRead(t *testing.T) { tests := []struct { name string quotedComments bool expected [][]string }{ { name: "Skip comments", quotedComments: true, expected: [][]string{ {"NAME", "ID", "SIZE", "MODIFIED"}, {"qwen3-coder:30b", "06c1097efce0", "18 GB", "8 days ago"}, {"gpt-oss:20b", "aa4295ac10c3", "13 GB", "8 days ago"}, {"gpt-oss:latest", "aa4295ac10c3", "13 GB", "7 weeks ago"}, }, }, { name: "Don't skip quoted comments", quotedComments: false, expected: [][]string{ {"# Sample Quoted Comment, with \"quotes\" itself"}, {"NAME", "ID", "SIZE", "MODIFIED"}, {"qwen3-coder:30b", "06c1097efce0", "18 GB", "8 days ago"}, {"gpt-oss:20b", "aa4295ac10c3", "13 GB", "8 days ago"}, {"gpt-oss:latest", "aa4295ac10c3", "13 GB", "7 weeks ago"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader := NewReader(strings.NewReader(sampleCSV)) reader.QuotedComments = tt.quotedComments for i, want := range tt.expected { got, err := reader.Read() if err != nil { t.Errorf("Read() error at record %d: %v", i, err) } if !slices.Equal(got, want) { t.Errorf("Read() record %d = %v, want %v", i, got, want) } } // Verify EOF _, err := reader.Read() if !errors.Is(err, io.EOF) { t.Errorf("Read() expected EOF, got %v", err) } }) } } // TestReadAll tests the ReadAll method for different configurations. func TestReadAll(t *testing.T) { tests := []struct { name string quotedComments bool expected [][]string }{ { name: "Skip comments", quotedComments: true, expected: [][]string{ {"NAME", "ID", "SIZE", "MODIFIED"}, {"qwen3-coder:30b", "06c1097efce0", "18 GB", "8 days ago"}, {"gpt-oss:20b", "aa4295ac10c3", "13 GB", "8 days ago"}, {"gpt-oss:latest", "aa4295ac10c3", "13 GB", "7 weeks ago"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader := NewReader(strings.NewReader(sampleCSV)) reader.QuotedComments = tt.quotedComments got, err := reader.ReadAll() if err != nil { t.Errorf("ReadAll() error: %v", err) } if len(got) != len(tt.expected) { t.Errorf("ReadAll() returned %d records, want %d", len(got), len(tt.expected)) } for i, want := range tt.expected { if !slices.Equal(got[i], want) { t.Errorf("ReadAll() record %d = %v, want %v", i, got[i], want) } } }) } } // TestNewReaderFromURLWithMalformedCSV tests NewReaderFromURL with malformed CSV. func TestNewReaderFromURLWithMalformedCSV(t *testing.T) { mockResp := &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(malformedCSV)), } client := &mockHTTPClient{resp: mockResp} originalGet := httpGet httpGet = client.Get defer func() { httpGet = originalGet }() url := "https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/edit?gid=559037238" reader := NewReaderFromURL(url) if reader.err != nil { t.Errorf("NewReaderFromURL() unexpected error: %v", reader.err) } // Reading should fail due to malformed CSV _, err := reader.Read() if err == nil { t.Error("Read() expected error for malformed CSV, got nil") } }