-
Notifications
You must be signed in to change notification settings - Fork 0
fix: prevent markdown converter hang and support label custom fields #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| // TestMarkdownToADF_IndentedCodeFenceInList reproduces a hang where an indented | ||
| // fenced code block inside an ordered/bulleted list caused parseParagraph to | ||
| // return consumed=0, leading to an infinite loop in parseBlocks. | ||
| // | ||
| // The fix has two parts: | ||
| // 1. parseBlocks must trim before checking for "```" so indented fences are | ||
| // parsed as code blocks (not as paragraphs). | ||
| // 2. parseParagraph must guarantee consumed >= 1 as a defensive backstop. | ||
| func TestMarkdownToADF_IndentedCodeFenceInList(t *testing.T) { | ||
| input := "1. Verify the token works:\n" + | ||
| " ```\n" + | ||
| " curl https://api.example.com/v1/me\n" + | ||
| " ```\n" + | ||
| "2. Next step.\n" | ||
|
|
||
| done := make(chan *ADF, 1) | ||
| go func() { | ||
| done <- MarkdownToADF(input) | ||
| }() | ||
|
|
||
| select { | ||
| case adf := <-done: | ||
| if adf == nil || adf.Type != "doc" { | ||
| t.Fatalf("expected doc ADF, got %+v", adf) | ||
| } | ||
| case <-time.After(2 * time.Second): | ||
| t.Fatal("MarkdownToADF hung on indented code fence inside ordered list") | ||
| } | ||
| } | ||
|
|
||
| // TestMarkdownToADF_IndentedCodeFenceTopLevel verifies that an indented code | ||
| // fence at the top level (4-space-indented ```) does not hang. Even if the | ||
| // dispatcher does not recognize it as a code block, parseParagraph must | ||
| // consume at least one line. | ||
| func TestMarkdownToADF_IndentedCodeFenceTopLevel(t *testing.T) { | ||
| input := "Header\n\n ```\n code\n ```\n\nFooter\n" | ||
|
|
||
| done := make(chan *ADF, 1) | ||
| go func() { | ||
| done <- MarkdownToADF(input) | ||
| }() | ||
|
|
||
| select { | ||
| case adf := <-done: | ||
| if adf == nil || adf.Type != "doc" { | ||
| t.Fatalf("expected doc ADF, got %+v", adf) | ||
| } | ||
| case <-time.After(2 * time.Second): | ||
| t.Fatal("MarkdownToADF hung on indented code fence") | ||
| } | ||
| } | ||
|
|
||
| // TestParseParagraph_NeverReturnsZero is a defensive guarantee that | ||
| // parseParagraph always consumes at least one line. Otherwise the parseBlocks | ||
| // loop can spin forever. | ||
| func TestParseParagraph_NeverReturnsZero(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| }{ | ||
| {"indented backticks", " ```"}, | ||
| {"indented heading marker", " # not a heading"}, | ||
| {"indented quote", " > not a quote"}, | ||
| {"indented bullet", " - not a bullet"}, | ||
| {"indented ordered", " 1. not a list"}, | ||
| {"indented hr", " ---"}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| lines := []string{tt.input} | ||
| _, consumed := parseParagraph(lines, 0) | ||
| if consumed < 1 { | ||
| t.Errorf("parseParagraph returned consumed=%d for %q (must be >= 1 to prevent infinite loops)", consumed, tt.input) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package issue | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "testing" | ||
|
|
||
| "github.com/enthus-appdev/atl-cli/internal/api" | ||
| ) | ||
|
|
||
| // TestCoerceFieldValue_LabelsCustomField reproduces a bug where label-typed | ||
| // custom fields (e.g. NX `Repo`, `Application`) were rejected by Jira because | ||
| // `--field "Repo=API"` sent the bare string "API" instead of the required | ||
| // `["API"]` array. The schema for label custom fields has: | ||
| // | ||
| // type = "array" | ||
| // items = "string" | ||
| // custom = "com.atlassian.jira.plugin.system.customfieldtypes:labels" | ||
| // | ||
| // The previous code only handled labels when Schema.Custom was empty, missing | ||
| // the custom-label case entirely. | ||
| func TestCoerceFieldValue_LabelsCustomField(t *testing.T) { | ||
| field := &api.Field{ | ||
| ID: "customfield_10410", | ||
| Name: "Repo", | ||
| Schema: &api.FieldSchema{ | ||
| Type: "array", | ||
| Items: "string", | ||
| Custom: "com.atlassian.jira.plugin.system.customfieldtypes:labels", | ||
| }, | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| value string | ||
| want []string | ||
| }{ | ||
| {"single label", "API", []string{"API"}}, | ||
| {"multiple labels comma-separated", "API,GUI,Portal", []string{"API", "GUI", "Portal"}}, | ||
| {"with spaces around commas", "API, GUI ,Portal", []string{"API", "GUI", "Portal"}}, | ||
| {"double commas dropped", "API,,GUI", []string{"API", "GUI"}}, | ||
| {"trailing comma dropped", "API,GUI,", []string{"API", "GUI"}}, | ||
| {"leading comma dropped", ",API,GUI", []string{"API", "GUI"}}, | ||
| {"whitespace-only entry dropped", "API, ,GUI", []string{"API", "GUI"}}, | ||
| {"all empty returns empty slice", ",,,", []string{}}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := coerceFieldValue(field, tt.value) | ||
| gotSlice, ok := got.([]string) | ||
| if !ok { | ||
| t.Fatalf("expected []string, got %T (%v)", got, got) | ||
| } | ||
| if !reflect.DeepEqual(gotSlice, tt.want) { | ||
| t.Errorf("coerceFieldValue(%q) = %v, want %v", tt.value, gotSlice, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestCoerceFieldValue_StandardLabelsField verifies the existing behavior for | ||
| // the standard system "labels" field (Schema.Custom == "") is preserved. | ||
| func TestCoerceFieldValue_StandardLabelsField(t *testing.T) { | ||
| field := &api.Field{ | ||
| ID: "labels", | ||
| Name: "Labels", | ||
| Schema: &api.FieldSchema{ | ||
| Type: "array", | ||
| Items: "string", | ||
| Custom: "", | ||
| }, | ||
| } | ||
|
|
||
| got := coerceFieldValue(field, "bug,urgent") | ||
| want := []string{"bug", "urgent"} | ||
| gotSlice, ok := got.([]string) | ||
| if !ok { | ||
| t.Fatalf("expected []string, got %T", got) | ||
| } | ||
| if !reflect.DeepEqual(gotSlice, want) { | ||
| t.Errorf("got %v, want %v", gotSlice, want) | ||
| } | ||
| } | ||
|
|
||
| // TestCoerceFieldValue_SelectStillWorks verifies select fields are not | ||
| // regressed by the new label detection. | ||
| func TestCoerceFieldValue_SelectStillWorks(t *testing.T) { | ||
| field := &api.Field{ | ||
| ID: "customfield_10412", | ||
| Name: "Ursprung des Fehlverhaltens", | ||
| Schema: &api.FieldSchema{ | ||
| Type: "option", | ||
| Custom: "com.atlassian.jira.plugin.system.customfieldtypes:select", | ||
| }, | ||
| } | ||
|
|
||
| got := coerceFieldValue(field, "aktuell") | ||
| want := map[string]string{"value": "aktuell"} | ||
| gotMap, ok := got.(map[string]string) | ||
| if !ok { | ||
| t.Fatalf("expected map[string]string, got %T", got) | ||
| } | ||
| if !reflect.DeepEqual(gotMap, want) { | ||
| t.Errorf("got %v, want %v", gotMap, want) | ||
| } | ||
| } | ||
|
|
||
| // TestCoerceFieldValue_RadioStillWorks verifies radiobutton fields are not regressed. | ||
| func TestCoerceFieldValue_RadioStillWorks(t *testing.T) { | ||
| field := &api.Field{ | ||
| ID: "customfield_10413", | ||
| Name: "Fehlverhalten", | ||
| Schema: &api.FieldSchema{ | ||
| Type: "option", | ||
| Custom: "com.atlassian.jira.plugin.system.customfieldtypes:radiobuttons", | ||
| }, | ||
| } | ||
|
|
||
| got := coerceFieldValue(field, "Ja") | ||
| want := map[string]string{"value": "Ja"} | ||
| gotMap, ok := got.(map[string]string) | ||
| if !ok { | ||
| t.Fatalf("expected map[string]string, got %T", got) | ||
| } | ||
| if !reflect.DeepEqual(gotMap, want) { | ||
| t.Errorf("got %v, want %v", gotMap, want) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.