Skip to content

Commit fb6e7a8

Browse files
committed
Add integration test and minor tweaks
1 parent 0ca0521 commit fb6e7a8

File tree

3 files changed

+174
-52
lines changed

3 files changed

+174
-52
lines changed

github/projects.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ type ProjectV2FieldOption struct {
9393
//
9494
// GitHub API docs: https://docs.github.com/rest/projects/fields
9595
type ProjectV2Field struct {
96-
ID *int64 `json:"id,omitempty"`
97-
NodeID string `json:"node_id,omitempty"`
98-
Name string `json:"name,omitempty"`
99-
DataType string `json:"dataType,omitempty"`
100-
URL string `json:"url,omitempty"`
101-
Options []*ProjectV2FieldOption `json:"options,omitempty"`
102-
CreatedAt *Timestamp `json:"created_at,omitempty"`
103-
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
96+
ID *int64 `json:"id,omitempty"`
97+
NodeID string `json:"node_id,omitempty"`
98+
Name string `json:"name,omitempty"`
99+
DataType string `json:"dataType,omitempty"`
100+
URL string `json:"url,omitempty"`
101+
Options []*any `json:"options,omitempty"`
102+
CreatedAt *Timestamp `json:"created_at,omitempty"`
103+
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
104104
}
105105

106106
// ListProjectsForOrg lists Projects V2 for an organization.

github/projects_test.go

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -265,15 +265,13 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) {
265265
t.Parallel()
266266
client, mux, _ := setup(t)
267267

268-
// Combined handler: supports initial test case and dual before/after validation scenario.
269268
mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) {
270269
testMethod(t, r, "GET")
271270
q := r.URL.Query()
272271
if q.Get("before") == "b" && q.Get("after") == "a" {
273272
fmt.Fprint(w, `[]`)
274273
return
275274
}
276-
// default expectation for main part of test
277275
testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"})
278276
fmt.Fprint(w, `[
279277
{
@@ -321,25 +319,37 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) {
321319
t.Fatalf("Projects.ListProjectFieldsForOrg returned %d fields, want 2", len(fields))
322320
}
323321

324-
// Validate first field (with options)
325322
field1 := fields[0]
326323
if field1.ID == nil || *field1.ID != 1 || field1.Name != "Status" || field1.DataType != "single_select" {
327-
t.Errorf("First field: got ID=%v, Name=%s, DataType=%s; want 1, Status, single_select",
328-
field1.ID, field1.Name, field1.DataType)
324+
t.Errorf("First field: got ID=%v, Name=%s, DataType=%s; want 1, Status, single_select", field1.ID, field1.Name, field1.DataType)
329325
}
330326
if len(field1.Options) != 2 {
331327
t.Errorf("First field options: got %d, want 2", len(field1.Options))
332-
}
333-
if field1.Options[0].Name != "Todo" || field1.Options[1].Name != "In Progress" {
334-
t.Errorf("First field option names: got %s, %s; want Todo, In Progress",
335-
field1.Options[0].Name, field1.Options[1].Name)
328+
} else {
329+
getName := func(o *any) string {
330+
if o == nil || *o == nil {
331+
return ""
332+
}
333+
switch v := (*o).(type) {
334+
case map[string]any:
335+
if n, ok := v["name"].(string); ok {
336+
return n
337+
}
338+
default:
339+
// fall back to fmt for debug; reflection can be added if needed.
340+
}
341+
return ""
342+
}
343+
name0, name1 := getName(field1.Options[0]), getName(field1.Options[1])
344+
if name0 != "Todo" || name1 != "In Progress" {
345+
t.Errorf("First field option names: got %q, %q; want Todo, In Progress", name0, name1)
346+
}
336347
}
337348

338349
// Validate second field (without options)
339350
field2 := fields[1]
340351
if field2.ID == nil || *field2.ID != 2 || field2.Name != "Priority" || field2.DataType != "text" {
341-
t.Errorf("Second field: got ID=%v, Name=%s, DataType=%s; want 2, Priority, text",
342-
field2.ID, field2.Name, field2.DataType)
352+
t.Errorf("Second field: got ID=%v, Name=%s, DataType=%s; want 2, Priority, text", field2.ID, field2.Name, field2.DataType)
343353
}
344354
if len(field2.Options) != 0 {
345355
t.Errorf("Second field options: got %d, want 0", len(field2.Options))
@@ -403,7 +413,6 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) {
403413
t.Fatalf("expected resp.After=cursor2 got %q", resp.After)
404414
}
405415

406-
// Use resp.After as opts.After for next page
407416
opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}}
408417
second, resp2, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts)
409418
if err != nil {
@@ -417,7 +426,6 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) {
417426
}
418427
}
419428

420-
// Marshal test ensures V2 fields marshal correctly.
421429
func TestProjectV2_Marshal(t *testing.T) {
422430
t.Parallel()
423431
testJSONMarshal(t, &ProjectV2{}, "{}")
@@ -443,47 +451,48 @@ func TestProjectV2_Marshal(t *testing.T) {
443451
testJSONMarshal(t, p, want)
444452
}
445453

446-
// Marshal test ensures V2 field structures marshal correctly.
447454
func TestProjectV2Field_Marshal(t *testing.T) {
448455
t.Parallel()
449-
testJSONMarshal(t, &ProjectV2Field{}, "{}")
450-
testJSONMarshal(t, &ProjectV2FieldOption{}, "{}")
456+
testJSONMarshal(t, &ProjectV2Field{}, "{}") // empty struct
457+
testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") // option struct still individually testable
458+
459+
type optStruct struct {
460+
Color string `json:"color,omitempty"`
461+
Description string `json:"description,omitempty"`
462+
ID string `json:"id,omitempty"`
463+
Name string `json:"name,omitempty"`
464+
}
465+
optVal := &optStruct{Color: "blue", Description: "Tasks to be done", ID: "option1", Name: "Todo"}
466+
var optAny any = optVal
451467

452468
field := &ProjectV2Field{
453-
ID: Ptr(int64(1)),
454-
NodeID: "node_1",
455-
Name: "Status",
456-
DataType: "single_select",
457-
URL: "https://api.github.com/projects/1/fields/field1",
458-
Options: []*ProjectV2FieldOption{
459-
{
460-
ID: "option1",
461-
Name: "Todo",
462-
Color: "blue",
463-
Description: "Tasks to be done",
464-
},
465-
},
469+
ID: Ptr(int64(1)),
470+
NodeID: "node_1",
471+
Name: "Status",
472+
DataType: "single_select",
473+
URL: "https://api.github.com/projects/1/fields/field1",
474+
Options: []*any{&optAny},
466475
CreatedAt: &Timestamp{referenceTime},
467476
UpdatedAt: &Timestamp{referenceTime},
468477
}
469478

470479
want := `{
471480
"id": 1,
472-
"node_id": "node_1",
473-
"name": "Status",
474-
"dataType": "single_select",
475-
"url": "https://api.github.com/projects/1/fields/field1",
476-
"options": [
477-
{
478-
"id": "option1",
479-
"name": "Todo",
480-
"color": "blue",
481-
"description": "Tasks to be done"
482-
}
483-
],
484-
"created_at": ` + referenceTimeStr + `,
485-
"updated_at": ` + referenceTimeStr + `
486-
}`
481+
"node_id": "node_1",
482+
"name": "Status",
483+
"dataType": "single_select",
484+
"url": "https://api.github.com/projects/1/fields/field1",
485+
"options": [
486+
{
487+
"id": "option1",
488+
"name": "Todo",
489+
"color": "blue",
490+
"description": "Tasks to be done"
491+
}
492+
],
493+
"created_at": ` + referenceTimeStr + `,
494+
"updated_at": ` + referenceTimeStr + `
495+
}`
487496

488497
testJSONMarshal(t, field, want)
489498
}

test/integration/projects_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2025 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
//go:build integration
7+
8+
package integration
9+
10+
import (
11+
"context"
12+
"os"
13+
"testing"
14+
15+
"github.com/google/go-github/v75/github"
16+
)
17+
18+
// Integration tests for Projects V2 endpoints defined in github/projects.go.
19+
//
20+
// These tests are intentionally defensive. They only require minimal
21+
// environment variables identifying a target org and user. Project numbers are
22+
// discovered dynamically by first listing projects and selecting one. For item
23+
// CRUD operations, the test creates a temporary repository & issue (where
24+
// possible) and adds/removes that issue as a project item. If prerequisites
25+
// (auth, env vars, permissions, presence of at least one project) are missing,
26+
// the relevant sub-test is skipped so other integration tests can still run.
27+
//
28+
// Required / optional environment variables:
29+
// GITHUB_AUTH_TOKEN (required for any of these tests to run)
30+
// GITHUB_TEST_ORG (org login; required for org project tests)
31+
// GITHUB_TEST_USER (user login; required for user project tests)
32+
// GITHUB_TEST_REPO (repo name)
33+
34+
func TestProjectsV2_Org(t *testing.T) {
35+
if !checkAuth("TestProjectsV2_Org") { // ensures client is authed
36+
return
37+
}
38+
org := os.Getenv("GITHUB_TEST_ORG")
39+
if org == "" {
40+
t.Skip("GITHUB_TEST_ORG not set")
41+
}
42+
43+
ctx := context.Background()
44+
45+
opts := &github.ListProjectsOptions{}
46+
// List projects for org; pick the first available project we can read.
47+
projects, _, err := client.Projects.ListProjectsForOrg(ctx, org, opts)
48+
if err != nil {
49+
// If listing itself fails, abort this test.
50+
t.Fatalf("Projects.ListProjectsForOrg returned error: %v", err)
51+
}
52+
if len(projects) == 0 {
53+
t.Skipf("no Projects V2 found for org %s", org)
54+
}
55+
project := projects[0]
56+
if project.Number == nil {
57+
t.Skip("selected org project has nil Number field")
58+
}
59+
projectNumber := int64(*project.Number)
60+
61+
// Re-fetch via Get to exercise endpoint explicitly.
62+
proj, _, err := client.Projects.GetProjectForOrg(ctx, org, projectNumber)
63+
if err != nil {
64+
// Permission mismatch? Skip CRUD while still reporting failure would make the test fail;
65+
// we want correctness so treat as fatal here.
66+
t.Fatalf("Projects.GetProjectForOrg returned error: %v", err)
67+
}
68+
if proj.Number == nil || int64(*proj.Number) != projectNumber {
69+
t.Fatalf("GetProjectForOrg returned unexpected project number: got %+v want %d", proj.Number, projectNumber)
70+
}
71+
72+
// List fields (may be empty)
73+
_, _, err = client.Projects.ListProjectFieldsForOrg(ctx, org, projectNumber, nil)
74+
if err != nil {
75+
// Fields listing might require extra perms; treat as fatal to surface potential regression.
76+
t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err)
77+
}
78+
}
79+
80+
func TestProjectsV2_User(t *testing.T) {
81+
if !checkAuth("TestProjectsV2_User") {
82+
return
83+
}
84+
user := os.Getenv("GITHUB_TEST_USER")
85+
if user == "" {
86+
t.Skip("GITHUB_TEST_USER not set")
87+
}
88+
89+
ctx := context.Background()
90+
opts := &github.ListProjectsOptions{}
91+
projects, _, err := client.Projects.ListProjectsForUser(ctx, user, opts)
92+
if err != nil {
93+
// Can't list user projects: fatal (indicates API or permission issue).
94+
t.Fatalf("Projects.ListProjectsForUser returned error: %v", err)
95+
}
96+
if len(projects) == 0 {
97+
t.Skipf("no Projects V2 found for user %s", user)
98+
}
99+
project := projects[0]
100+
if project.Number == nil {
101+
t.Skip("selected user project has nil Number field")
102+
}
103+
projectNumber := int64(*project.Number)
104+
105+
proj, _, err := client.Projects.GetProjectForUser(ctx, user, projectNumber)
106+
if err != nil {
107+
// can't fetch specific project; treat as fatal
108+
t.Fatalf("Projects.GetProjectForUser returned error: %v", err)
109+
}
110+
if proj.Number == nil || int64(*proj.Number) != projectNumber {
111+
t.Fatalf("GetProjectForUser returned unexpected project number: got %+v want %d", proj.Number, projectNumber)
112+
}
113+
}

0 commit comments

Comments
 (0)