package migration import ( "context" "net/http" "strings" "testing" ) func TestGraphWellKnownFolder(t *testing.T) { remote, ftype := graphWellKnownFolder("inbox", "Inbox") if remote != "INBOX" || ftype != "inbox" { t.Fatalf("got %q / %q", remote, ftype) } remote, ftype = graphWellKnownFolder("", "Projects") if remote != "PROJECTS" || ftype != "custom" { t.Fatalf("custom folder: got %q / %q", remote, ftype) } } func TestGraphFlags(t *testing.T) { flags := graphFlags(true, "notFlagged") if len(flags) != 1 || flags[0] != "\\Seen" { t.Fatalf("read flags: %v", flags) } flags = graphFlags(false, "flagged") if len(flags) != 1 || flags[0] != "\\Flagged" { t.Fatalf("flagged: %v", flags) } } func TestGraphRecipientsJSON(t *testing.T) { raw := graphRecipientsJSON([]graphRecipient{ {EmailAddress: graphEmailAddress{Name: "Bob", Address: "bob@example.com"}}, }) if string(raw) != `[{"name":"Bob","email":"bob@example.com"}]` { t.Fatalf("unexpected json: %s", raw) } } func TestParseGraphTime(t *testing.T) { tm := parseGraphTime("2024-05-01T12:34:56Z") if tm.IsZero() { t.Fatal("expected parsed time") } } func TestRemoteMessageUIDMatchesGmailUID(t *testing.T) { id := "abc123" if remoteMessageUID(id) != gmailUID(id) { t.Fatal("uid helpers diverged") } } func TestGraphFolderQueueSortedAndCached(t *testing.T) { g := NewGraphImporter(nil) g.folders = map[string]graphFolderMeta{ "sent-folder": {RemoteName: "SENT", FolderType: "sent"}, "inbox-folder": {RemoteName: "INBOX", FolderType: "inbox"}, } cursor := map[string]any{} queue := g.folderQueue(cursor) if len(queue) != 2 { t.Fatalf("queue len = %d", len(queue)) } if queue[0] != "inbox-folder" || queue[1] != "sent-folder" { t.Fatalf("queue order = %v", queue) } cached := readGraphFolderQueue(cursor) if len(cached) != 2 || cached[0] != "inbox-folder" { t.Fatalf("cached queue = %v", cached) } } func TestGraphFolderMessagesURLUsesMailFoldersPath(t *testing.T) { g := NewGraphImporter(nil).WithBaseURL("https://graph.test") listURL := g.folderMessagesURL("folder-abc") if !strings.Contains(listURL, "/mailFolders/folder-abc/messages") { t.Fatalf("url = %q", listURL) } if strings.Contains(listURL, "/me/messages") { t.Fatalf("flat messages path should not be used: %q", listURL) } } func TestGraphEnsureFoldersPaginates(t *testing.T) { pages := 0 client := mockGraphHTTPClient(t, func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/childFolders") { _, _ = w.Write([]byte(`{"value":[]}`)) return } if !strings.HasSuffix(r.URL.Path, "/mailFolders") { http.NotFound(w, r) return } pages++ if pages == 1 { _, _ = w.Write([]byte(`{ "value":[{"id":"inbox-id","displayName":"Inbox","wellKnownName":"inbox"}], "@odata.nextLink":"https://graph.microsoft.com/v1.0/me/mailFolders?$top=100&$skip=100" }`)) return } _, _ = w.Write([]byte(`{"value":[{"id":"sent-id","displayName":"Sent","wellKnownName":"sentitems"}]}`)) }) g := NewGraphImporter(nil).WithHTTPClient(client) if err := g.ensureGraphFolders(context.Background(), "token"); err != nil { t.Fatalf("ensure folders: %v", err) } if pages != 2 { t.Fatalf("pages = %d, want 2", pages) } if len(g.folders) != 2 { t.Fatalf("folders = %d", len(g.folders)) } } func TestGraphEnsureFoldersDiscoversNestedChildFolders(t *testing.T) { childCalls := 0 client := mockGraphHTTPClient(t, func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/mailFolders"): _, _ = w.Write([]byte(`{"value":[ {"id":"inbox-id","displayName":"Inbox","wellKnownName":"inbox"} ]}`)) case strings.Contains(r.URL.Path, "/mailFolders/inbox-id/childFolders"): childCalls++ _, _ = w.Write([]byte(`{"value":[ {"id":"projects-id","displayName":"Projects","wellKnownName":""} ]}`)) case strings.Contains(r.URL.Path, "/mailFolders/projects-id/childFolders"): childCalls++ _, _ = w.Write([]byte(`{"value":[ {"id":"nested-id","displayName":"2024","wellKnownName":""} ]}`)) case strings.Contains(r.URL.Path, "/mailFolders/nested-id/childFolders"): childCalls++ _, _ = w.Write([]byte(`{"value":[]}`)) default: http.NotFound(w, r) } }) g := NewGraphImporter(nil).WithHTTPClient(client) if err := g.ensureGraphFolders(context.Background(), "token"); err != nil { t.Fatalf("ensure folders: %v", err) } if childCalls != 3 { t.Fatalf("childFolders calls = %d, want 3", childCalls) } if len(g.folders) != 3 { t.Fatalf("folders = %d, want 3", len(g.folders)) } projects := g.folders["projects-id"] if projects.RemoteName != "INBOX/PROJECTS" || projects.FolderType != "custom" { t.Fatalf("projects meta = %+v", projects) } nested := g.folders["nested-id"] if nested.RemoteName != "INBOX/PROJECTS/2024" || nested.FolderType != "custom" { t.Fatalf("nested meta = %+v", nested) } } func TestGraphEnsureFoldersChildFoldersPaginate(t *testing.T) { pages := 0 client := mockGraphHTTPClient(t, func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/mailFolders"): _, _ = w.Write([]byte(`{"value":[ {"id":"inbox-id","displayName":"Inbox","wellKnownName":"inbox"} ]}`)) case strings.Contains(r.URL.Path, "/mailFolders/inbox-id/childFolders"): pages++ if pages == 1 { _, _ = w.Write([]byte(`{ "value":[{"id":"child-a","displayName":"Alpha","wellKnownName":""}], "@odata.nextLink":"https://graph.microsoft.com/v1.0/me/mailFolders/inbox-id/childFolders?$skip=100" }`)) return } _, _ = w.Write([]byte(`{"value":[{"id":"child-b","displayName":"Beta","wellKnownName":""}]}`)) case strings.Contains(r.URL.Path, "/childFolders"): _, _ = w.Write([]byte(`{"value":[]}`)) default: http.NotFound(w, r) } }) g := NewGraphImporter(nil).WithHTTPClient(client) if err := g.ensureGraphFolders(context.Background(), "token"); err != nil { t.Fatalf("ensure folders: %v", err) } if pages != 2 { t.Fatalf("child pages = %d, want 2", pages) } if g.folders["child-a"].RemoteName != "INBOX/ALPHA" { t.Fatalf("child-a remote = %q", g.folders["child-a"].RemoteName) } if g.folders["child-b"].RemoteName != "INBOX/BETA" { t.Fatalf("child-b remote = %q", g.folders["child-b"].RemoteName) } } func TestGraphNestedFolderMeta(t *testing.T) { remote, ftype := graphNestedFolderMeta("INBOX", "", "My Folder") if remote != "INBOX/MY_FOLDER" || ftype != "custom" { t.Fatalf("got %q / %q", remote, ftype) } remote, ftype = graphNestedFolderMeta("", "", "Top") if remote != "TOP" || ftype != "custom" { t.Fatalf("top-level custom: got %q / %q", remote, ftype) } } func TestGraphFolderQueueIncludesNestedFolders(t *testing.T) { g := NewGraphImporter(nil) g.folders = map[string]graphFolderMeta{ "inbox-id": {RemoteName: "INBOX", FolderType: "inbox"}, "projects-id": {RemoteName: "INBOX/PROJECTS", FolderType: "custom"}, "archive-id": {RemoteName: "ARCHIVE", FolderType: "archive"}, } cursor := map[string]any{} queue := g.folderQueue(cursor) if len(queue) != 3 { t.Fatalf("queue len = %d", len(queue)) } } func TestGraphFolderQueueMergesLegacyCursor(t *testing.T) { g := NewGraphImporter(nil) g.folders = map[string]graphFolderMeta{ "inbox-id": {RemoteName: "INBOX", FolderType: "inbox"}, "sent-id": {RemoteName: "SENT", FolderType: "sent"}, "projects-id": {RemoteName: "INBOX/PROJECTS", FolderType: "custom", ParentGraphID: "inbox-id"}, } cursor := map[string]any{ "graphFolderQueue": []any{"inbox-id", "sent-id"}, "folderIndex": float64(2), } queue := g.folderQueue(cursor) if len(queue) != 3 { t.Fatalf("queue len = %d, want 3", len(queue)) } if queue[0] != "inbox-id" || queue[1] != "sent-id" || queue[2] != "projects-id" { t.Fatalf("queue order = %v", queue) } } func TestMergeGraphFolderQueuePreservesOrder(t *testing.T) { cursor := map[string]any{ "graphFolderQueue": []any{"b-folder", "a-folder"}, } merged := mergeGraphFolderQueue(cursor, []string{"a-folder", "b-folder", "c-folder"}) if len(merged) != 3 { t.Fatalf("merged len = %d", len(merged)) } if merged[0] != "b-folder" || merged[1] != "a-folder" || merged[2] != "c-folder" { t.Fatalf("merged order = %v", merged) } } func TestMergeGraphFolderQueueNoOpWhenComplete(t *testing.T) { cursor := map[string]any{ "graphFolderQueue": []any{"inbox-id", "sent-id"}, } merged := mergeGraphFolderQueue(cursor, []string{"inbox-id", "sent-id"}) if len(merged) != 2 { t.Fatalf("merged len = %d", len(merged)) } if merged[0] != "inbox-id" || merged[1] != "sent-id" { t.Fatalf("merged = %v", merged) } } func TestGraphInitFolderDeltaLink(t *testing.T) { client := mockGraphHTTPClient(t, func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/mailFolders/inbox-id/messages/delta") { _, _ = w.Write([]byte(`{"@odata.deltaLink":"https://graph.microsoft.com/v1.0/me/mailFolders/inbox-id/messages/delta?token=done"}`)) return } http.NotFound(w, r) }) g := NewGraphImporter(nil).WithHTTPClient(client) link, err := g.initFolderDeltaLink(context.Background(), "token", "inbox-id") if err != nil { t.Fatalf("init delta: %v", err) } if !strings.Contains(link, "/mailFolders/inbox-id/messages/delta") { t.Fatalf("delta link = %q", link) } } func TestGraphFolderDeltaLinkHelpers(t *testing.T) { cursor := map[string]any{} setGraphFolderDeltaLink(cursor, "inbox-id", "https://delta/inbox") links := graphFolderDeltaLinks(cursor) if links["inbox-id"] != "https://delta/inbox" { t.Fatalf("links = %v", links) } }