diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6eb1d0b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Download dependencies + run: go mod download + + - name: Generate templ files + run: go generate ./modules/... + + - name: Run tests with race detector + run: go test ./... -race \ No newline at end of file diff --git a/engine/errors_test.go b/engine/errors_test.go new file mode 100644 index 0000000..056d3f8 --- /dev/null +++ b/engine/errors_test.go @@ -0,0 +1,63 @@ +package engine + +import ( + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderError(t *testing.T) { + tests := []struct { + name string + error *httpError + fixtureName string + }{ + { + name: "client_error_400", + error: &httpError{ + StatusCode: 400, + Message: "Invalid request parameter", + }, + fixtureName: "_client_error", + }, + { + name: "client_error_404", + error: &httpError{ + StatusCode: 404, + Message: "Page not found", + }, + fixtureName: "_not_found", + }, + { + name: "server_error_500", + error: &httpError{ + StatusCode: 500, + Message: "Internal server error", + }, + fixtureName: "_server_error", + }, + { + name: "server_error_502", + error: &httpError{ + StatusCode: 502, + Message: "Bad gateway", + }, + fixtureName: "_bad_gateway", + }, + { + name: "edge_case_499", + error: &httpError{ + StatusCode: 499, + Message: "Client closed request", + }, + fixtureName: "_edge_case_499", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderError(tt.error) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/engine/fixtures/TestRenderError_client_error_400_client_error.html b/engine/fixtures/TestRenderError_client_error_400_client_error.html new file mode 100644 index 0000000..b9ec85a --- /dev/null +++ b/engine/fixtures/TestRenderError_client_error_400_client_error.html @@ -0,0 +1 @@ +Conway Makerspace System

Bad Request

Invalid request parameter
\ No newline at end of file diff --git a/engine/fixtures/TestRenderError_client_error_404_not_found.html b/engine/fixtures/TestRenderError_client_error_404_not_found.html new file mode 100644 index 0000000..b6f2261 --- /dev/null +++ b/engine/fixtures/TestRenderError_client_error_404_not_found.html @@ -0,0 +1 @@ +Conway Makerspace System

Bad Request

Page not found
\ No newline at end of file diff --git a/engine/fixtures/TestRenderError_edge_case_499_edge_case_499.html b/engine/fixtures/TestRenderError_edge_case_499_edge_case_499.html new file mode 100644 index 0000000..ca58f59 --- /dev/null +++ b/engine/fixtures/TestRenderError_edge_case_499_edge_case_499.html @@ -0,0 +1 @@ +Conway Makerspace System

Bad Request

Client closed request
\ No newline at end of file diff --git a/engine/fixtures/TestRenderError_server_error_500_server_error.html b/engine/fixtures/TestRenderError_server_error_500_server_error.html new file mode 100644 index 0000000..373945a --- /dev/null +++ b/engine/fixtures/TestRenderError_server_error_500_server_error.html @@ -0,0 +1 @@ +Conway Makerspace System

Uh oh

Internal server error
\ No newline at end of file diff --git a/engine/fixtures/TestRenderError_server_error_502_bad_gateway.html b/engine/fixtures/TestRenderError_server_error_502_bad_gateway.html new file mode 100644 index 0000000..3c57f18 --- /dev/null +++ b/engine/fixtures/TestRenderError_server_error_502_bad_gateway.html @@ -0,0 +1 @@ +Conway Makerspace System

Uh oh

Bad gateway
\ No newline at end of file diff --git a/internal/testing/templates.go b/internal/testing/templates.go new file mode 100644 index 0000000..d054c20 --- /dev/null +++ b/internal/testing/templates.go @@ -0,0 +1,55 @@ +package testing + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/a-h/templ" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// RenderSnapshot tests a templ component against a snapshot fixture. +// If RENDER_SNAPSHOTS environment variable is set, it will write/update the fixture file. +// Otherwise, it will compare the rendered output against the existing fixture. +func RenderSnapshot(t *testing.T, component templ.Component, fixturePath string) { + t.Helper() + + // Render the component to string + var buf strings.Builder + err := component.Render(context.Background(), &buf) + require.NoError(t, err, "Failed to render template component") + + rendered := buf.String() + + if os.Getenv("RENDER_SNAPSHOTS") != "" { + // Write mode: create/update fixture files + err := os.MkdirAll(filepath.Dir(fixturePath), 0755) + require.NoError(t, err, "Failed to create fixture directory") + + err = os.WriteFile(fixturePath, []byte(rendered), 0644) + require.NoError(t, err, "Failed to write fixture file") + + t.Logf("Updated fixture: %s", fixturePath) + return + } + + // Test mode: compare against existing fixture + expected, err := os.ReadFile(fixturePath) + require.NoError(t, err, "Failed to read fixture file: %s. Run tests with RENDER_SNAPSHOTS=1 to generate it.", fixturePath) + + assert.Equal(t, string(expected), rendered, "Rendered output does not match fixture: %s", fixturePath) +} + +// RenderSnapshotWithName is a convenience function that generates a fixture path +// based on the test name and an optional suffix. +func RenderSnapshotWithName(t *testing.T, component templ.Component, suffix string) { + t.Helper() + + testName := strings.ReplaceAll(t.Name(), "/", "_") + fixturePath := filepath.Join("fixtures", testName+suffix+".html") + RenderSnapshot(t, component, fixturePath) +} \ No newline at end of file diff --git a/modules/admin/admin_test.go b/modules/admin/admin_test.go new file mode 100644 index 0000000..aefd288 --- /dev/null +++ b/modules/admin/admin_test.go @@ -0,0 +1,411 @@ +package admin + +import ( + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestAdminNav(t *testing.T) { + tests := []struct { + name string + tabs []*navbarTab + fixtureName string + description string + }{ + { + name: "empty_tabs", + tabs: []*navbarTab{}, + fixtureName: "_empty", + description: "Navigation with no tabs", + }, + { + name: "single_tab", + tabs: []*navbarTab{ + {Title: "Members", Path: "/admin/members"}, + }, + fixtureName: "_single", + description: "Navigation with single tab", + }, + { + name: "multiple_tabs", + tabs: []*navbarTab{ + {Title: "Members", Path: "/admin/members"}, + {Title: "Events", Path: "/admin/events"}, + {Title: "Settings", Path: "/admin/settings"}, + }, + fixtureName: "_multiple", + description: "Navigation with multiple tabs", + }, + { + name: "special_chars_tabs", + tabs: []*navbarTab{ + {Title: "M&M's Report", Path: "/admin/reports?type=candy"}, + {Title: "Café & Lounge", Path: "/admin/areas/café"}, + {Title: "R&D Projects", Path: "/admin/projects?dept=r&d"}, + }, + fixtureName: "_special_chars", + description: "Navigation with special characters in titles and paths", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := adminNav(tt.tabs) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestTableCellRender(t *testing.T) { + tests := []struct { + name string + cell tableCell + row *tableRow + fixtureName string + description string + }{ + { + name: "text_cell", + cell: tableCell{ + Text: "Simple Text", + BadgeType: "", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/items/123", + Cells: []*tableCell{}, + }, + fixtureName: "_text", + description: "Basic text cell with no special formatting", + }, + { + name: "badge_primary", + cell: tableCell{ + Text: "Active", + BadgeType: "primary", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/items/123", + Cells: []*tableCell{}, + }, + fixtureName: "_badge_primary", + description: "Cell with primary badge", + }, + { + name: "badge_success", + cell: tableCell{ + Text: "Ready", + BadgeType: "success", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/members/456", + Cells: []*tableCell{}, + }, + fixtureName: "_badge_success", + description: "Cell with success badge", + }, + { + name: "badge_danger", + cell: tableCell{ + Text: "Inactive", + BadgeType: "danger", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/members/789", + Cells: []*tableCell{}, + }, + fixtureName: "_badge_danger", + description: "Cell with danger badge", + }, + { + name: "badge_warning", + cell: tableCell{ + Text: "Pending", + BadgeType: "warning", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/requests/101", + Cells: []*tableCell{}, + }, + fixtureName: "_badge_warning", + description: "Cell with warning badge", + }, + { + name: "self_link_button", + cell: tableCell{ + Text: "", + BadgeType: "", + SelfLinkButton: "Edit", + }, + row: &tableRow{ + SelfLink: "/admin/items/456", + Cells: []*tableCell{}, + }, + fixtureName: "_button", + description: "Cell with edit button linking to item", + }, + { + name: "empty_cell", + cell: tableCell{ + Text: "", + BadgeType: "", + SelfLinkButton: "", + }, + row: &tableRow{ + SelfLink: "/admin/empty/1", + Cells: []*tableCell{}, + }, + fixtureName: "_empty", + description: "Empty cell with default rendering", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := tt.cell.Render(tt.row) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderAdminList(t *testing.T) { + tests := []struct { + name string + tabs []*navbarTab + typeName string + searchURL string + fixtureName string + description string + }{ + { + name: "members_list", + tabs: []*navbarTab{ + {Title: "Members", Path: "/admin/members"}, + {Title: "Events", Path: "/admin/events"}, + }, + typeName: "Members", + searchURL: "/admin/members/search", + fixtureName: "_members", + description: "Admin list for members with navigation", + }, + { + name: "events_list", + tabs: []*navbarTab{ + {Title: "Members", Path: "/admin/members"}, + {Title: "Events", Path: "/admin/events"}, + }, + typeName: "Events", + searchURL: "/admin/events/search", + fixtureName: "_events", + description: "Admin list for events", + }, + { + name: "minimal_list", + tabs: []*navbarTab{}, + typeName: "Items", + searchURL: "/search", + fixtureName: "_minimal", + description: "Minimal admin list with no navigation tabs", + }, + { + name: "complex_search_url", + tabs: []*navbarTab{ + {Title: "Reports", Path: "/admin/reports"}, + }, + typeName: "Transaction Reports", + searchURL: "/admin/reports/search?filter=transactions&period=monthly", + fixtureName: "_complex_search", + description: "Admin list with complex search URL", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderAdminList(tt.tabs, tt.typeName, tt.searchURL) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderAdminListElements(t *testing.T) { + tests := []struct { + name string + rowMeta []*tableRowMeta + rows []*tableRow + currentPage int64 + totalPages int64 + fixtureName string + description string + }{ + { + name: "empty_list", + rowMeta: []*tableRowMeta{}, + rows: []*tableRow{}, + currentPage: 1, + totalPages: 1, + fixtureName: "_empty", + description: "Empty list with no data", + }, + { + name: "simple_list", + rowMeta: []*tableRowMeta{ + {Title: "Name", Width: 6}, + {Title: "Status", Width: 3}, + {Title: "Actions", Width: 3}, + }, + rows: []*tableRow{ + { + SelfLink: "/admin/items/1", + Cells: []*tableCell{ + {Text: "Item One"}, + {Text: "Active", BadgeType: "success"}, + {SelfLinkButton: "Edit"}, + }, + }, + { + SelfLink: "/admin/items/2", + Cells: []*tableCell{ + {Text: "Item Two"}, + {Text: "Pending", BadgeType: "warning"}, + {SelfLinkButton: "Edit"}, + }, + }, + }, + currentPage: 1, + totalPages: 1, + fixtureName: "_simple", + description: "Simple list with data", + }, + { + name: "paginated_list_first_page", + rowMeta: []*tableRowMeta{ + {Title: "ID", Width: 2}, + {Title: "Email", Width: 8}, + {Title: "Status", Width: 2}, + }, + rows: []*tableRow{ + { + SelfLink: "/admin/members/1", + Cells: []*tableCell{ + {Text: "1"}, + {Text: "user1@example.com"}, + {Text: "Ready", BadgeType: "success"}, + }, + }, + }, + currentPage: 1, + totalPages: 5, + fixtureName: "_paginated_first", + description: "First page of paginated list", + }, + { + name: "paginated_list_middle_page", + rowMeta: []*tableRowMeta{ + {Title: "ID", Width: 2}, + {Title: "Email", Width: 8}, + {Title: "Status", Width: 2}, + }, + rows: []*tableRow{ + { + SelfLink: "/admin/members/11", + Cells: []*tableCell{ + {Text: "11"}, + {Text: "user11@example.com"}, + {Text: "Active", BadgeType: "primary"}, + }, + }, + }, + currentPage: 3, + totalPages: 5, + fixtureName: "_paginated_middle", + description: "Middle page of paginated list", + }, + { + name: "paginated_list_last_page", + rowMeta: []*tableRowMeta{ + {Title: "ID", Width: 2}, + {Title: "Email", Width: 8}, + {Title: "Status", Width: 2}, + }, + rows: []*tableRow{ + { + SelfLink: "/admin/members/25", + Cells: []*tableCell{ + {Text: "25"}, + {Text: "user25@example.com"}, + {Text: "Inactive", BadgeType: "danger"}, + }, + }, + }, + currentPage: 5, + totalPages: 5, + fixtureName: "_paginated_last", + description: "Last page of paginated list", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderAdminListElements(tt.rowMeta, tt.rows, tt.currentPage, tt.totalPages) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderListPagination(t *testing.T) { + tests := []struct { + name string + currentPage int64 + totalPages int64 + fixtureName string + description string + }{ + { + name: "single_page", + currentPage: 1, + totalPages: 1, + fixtureName: "_single", + description: "Single page pagination", + }, + { + name: "first_page_multiple", + currentPage: 1, + totalPages: 5, + fixtureName: "_first_multiple", + description: "First page of multiple pages", + }, + { + name: "middle_page", + currentPage: 3, + totalPages: 5, + fixtureName: "_middle", + description: "Middle page of multiple pages", + }, + { + name: "last_page", + currentPage: 5, + totalPages: 5, + fixtureName: "_last", + description: "Last page of multiple pages", + }, + { + name: "large_pagination", + currentPage: 50, + totalPages: 100, + fixtureName: "_large", + description: "Large pagination with many pages", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderListPagination(tt.currentPage, tt.totalPages) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/admin/fixtures/TestAdminNav_empty_tabs_empty.html b/modules/admin/fixtures/TestAdminNav_empty_tabs_empty.html new file mode 100644 index 0000000..7b23630 --- /dev/null +++ b/modules/admin/fixtures/TestAdminNav_empty_tabs_empty.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/admin/fixtures/TestAdminNav_multiple_tabs_multiple.html b/modules/admin/fixtures/TestAdminNav_multiple_tabs_multiple.html new file mode 100644 index 0000000..5032ebf --- /dev/null +++ b/modules/admin/fixtures/TestAdminNav_multiple_tabs_multiple.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/admin/fixtures/TestAdminNav_single_tab_single.html b/modules/admin/fixtures/TestAdminNav_single_tab_single.html new file mode 100644 index 0000000..8b03743 --- /dev/null +++ b/modules/admin/fixtures/TestAdminNav_single_tab_single.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/admin/fixtures/TestAdminNav_special_chars_tabs_special_chars.html b/modules/admin/fixtures/TestAdminNav_special_chars_tabs_special_chars.html new file mode 100644 index 0000000..3b0aca3 --- /dev/null +++ b/modules/admin/fixtures/TestAdminNav_special_chars_tabs_special_chars.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminListElements_empty_list_empty.html b/modules/admin/fixtures/TestRenderAdminListElements_empty_list_empty.html new file mode 100644 index 0000000..b51b098 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminListElements_empty_list_empty.html @@ -0,0 +1,8 @@ +
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_first_page_paginated_first.html b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_first_page_paginated_first.html new file mode 100644 index 0000000..a2b4656 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_first_page_paginated_first.html @@ -0,0 +1,9 @@ +
IDEmailStatus
1user1@example.comReady
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_last_page_paginated_last.html b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_last_page_paginated_last.html new file mode 100644 index 0000000..d4cbf95 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_last_page_paginated_last.html @@ -0,0 +1,9 @@ +
IDEmailStatus
25user25@example.comInactive
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_middle_page_paginated_middle.html b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_middle_page_paginated_middle.html new file mode 100644 index 0000000..12e629b --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminListElements_paginated_list_middle_page_paginated_middle.html @@ -0,0 +1,9 @@ +
IDEmailStatus
11user11@example.comActive
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminListElements_simple_list_simple.html b/modules/admin/fixtures/TestRenderAdminListElements_simple_list_simple.html new file mode 100644 index 0000000..b8868df --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminListElements_simple_list_simple.html @@ -0,0 +1,9 @@ +
NameStatusActions
Item OneActiveEdit
Item TwoPendingEdit
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminList_complex_search_url_complex_search.html b/modules/admin/fixtures/TestRenderAdminList_complex_search_url_complex_search.html new file mode 100644 index 0000000..8195801 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminList_complex_search_url_complex_search.html @@ -0,0 +1 @@ +Conway Makerspace System

Transaction Reports

\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminList_events_list_events.html b/modules/admin/fixtures/TestRenderAdminList_events_list_events.html new file mode 100644 index 0000000..5c68693 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminList_events_list_events.html @@ -0,0 +1 @@ +Conway Makerspace System

Events

\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminList_members_list_members.html b/modules/admin/fixtures/TestRenderAdminList_members_list_members.html new file mode 100644 index 0000000..9b70177 --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminList_members_list_members.html @@ -0,0 +1 @@ +Conway Makerspace System

Members

\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderAdminList_minimal_list_minimal.html b/modules/admin/fixtures/TestRenderAdminList_minimal_list_minimal.html new file mode 100644 index 0000000..4881b2b --- /dev/null +++ b/modules/admin/fixtures/TestRenderAdminList_minimal_list_minimal.html @@ -0,0 +1 @@ +Conway Makerspace System

Items

\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderListPagination_first_page_multiple_first_multiple.html b/modules/admin/fixtures/TestRenderListPagination_first_page_multiple_first_multiple.html new file mode 100644 index 0000000..9e2f922 --- /dev/null +++ b/modules/admin/fixtures/TestRenderListPagination_first_page_multiple_first_multiple.html @@ -0,0 +1,8 @@ +
Previous Page 1 of 5 Next
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderListPagination_large_pagination_large.html b/modules/admin/fixtures/TestRenderListPagination_large_pagination_large.html new file mode 100644 index 0000000..e15a6e8 --- /dev/null +++ b/modules/admin/fixtures/TestRenderListPagination_large_pagination_large.html @@ -0,0 +1,8 @@ +
Previous Page 50 of 100 Next
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderListPagination_last_page_last.html b/modules/admin/fixtures/TestRenderListPagination_last_page_last.html new file mode 100644 index 0000000..1f458e5 --- /dev/null +++ b/modules/admin/fixtures/TestRenderListPagination_last_page_last.html @@ -0,0 +1,8 @@ +
Previous Page 5 of 5 Next
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderListPagination_middle_page_middle.html b/modules/admin/fixtures/TestRenderListPagination_middle_page_middle.html new file mode 100644 index 0000000..42d1256 --- /dev/null +++ b/modules/admin/fixtures/TestRenderListPagination_middle_page_middle.html @@ -0,0 +1,8 @@ +
Previous Page 3 of 5 Next
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderListPagination_single_page_single.html b/modules/admin/fixtures/TestRenderListPagination_single_page_single.html new file mode 100644 index 0000000..30610fe --- /dev/null +++ b/modules/admin/fixtures/TestRenderListPagination_single_page_single.html @@ -0,0 +1,8 @@ +
Previous Page 1 of 1 Next
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_all_discounts_educator_discount.html b/modules/admin/fixtures/TestRenderSingleMember_all_discounts_educator_discount.html new file mode 100644 index 0000000..ef717fb --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_all_discounts_educator_discount.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_complete_member_active_complete_active.html b/modules/admin/fixtures/TestRenderSingleMember_complete_member_active_complete_active.html new file mode 100644 index 0000000..2a6225f --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_complete_member_active_complete_active.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Last fob'd in on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe
Subscription status: active
View subscription in Stripe
Designations
Events
TimeEventDetails
2023-06-15T11:00:00ZMembershipActivatedMembership activated via Stripe
2023-06-15T10:00:00ZPaymentReceivedMonthly payment processed
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_family_discount_family_discount.html b/modules/admin/fixtures/TestRenderSingleMember_family_discount_family_discount.html new file mode 100644 index 0000000..0b52688 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_family_discount_family_discount.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_inactive_member_inactive.html b/modules/admin/fixtures/TestRenderSingleMember_inactive_member_inactive.html new file mode 100644 index 0000000..9c86376 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_inactive_member_inactive.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Tue, May 16 2023
Last fob'd in on Tue, May 16 2023
Login code
PaymentInactive
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
2023-06-14T12:00:00ZPaymentFailedCredit card declined
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_many_events_many_events.html b/modules/admin/fixtures/TestRenderSingleMember_many_events_many_events.html new file mode 100644 index 0000000..b440995 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_many_events_many_events.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
Only the 10 most recent events are shown
TimeEventDetails
2023-06-15T11:00:00ZEvent1Details1
2023-06-15T10:00:00ZEvent2Details2
2023-06-15T09:00:00ZEvent3Details3
2023-06-15T08:00:00ZEvent4Details4
2023-06-15T07:00:00ZEvent5Details5
2023-06-15T06:00:00ZEvent6Details6
2023-06-15T05:00:00ZEvent7Details7
2023-06-15T04:00:00ZEvent8Details8
2023-06-15T03:00:00ZEvent9Details9
2023-06-15T02:00:00ZEvent10Details10
2023-06-15T01:00:00ZEvent11Details11
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_minimal_member_minimal.html b/modules/admin/fixtures/TestRenderSingleMember_minimal_member_minimal.html new file mode 100644 index 0000000..f354340 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_minimal_member_minimal.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_non_billable_leadership_leadership_nonbillable.html b/modules/admin/fixtures/TestRenderSingleMember_non_billable_leadership_leadership_nonbillable.html new file mode 100644 index 0000000..e8f566f --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_non_billable_leadership_leadership_nonbillable.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_paypal_member_paypal.html b/modules/admin/fixtures/TestRenderSingleMember_paypal_member_paypal.html new file mode 100644 index 0000000..7579422 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_paypal_member_paypal.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Paypal
Subscription price: 75.00
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_stripe_empty_status_stripe_empty.html b/modules/admin/fixtures/TestRenderSingleMember_stripe_empty_status_stripe_empty.html new file mode 100644 index 0000000..55c8761 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_stripe_empty_status_stripe_empty.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_stripe_inactive_no_sub_stripe_no_sub.html b/modules/admin/fixtures/TestRenderSingleMember_stripe_inactive_no_sub_stripe_no_sub.html new file mode 100644 index 0000000..b6642ce --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_stripe_inactive_no_sub_stripe_no_sub.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: canceled
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestRenderSingleMember_stripe_unknown_status_stripe_unknown.html b/modules/admin/fixtures/TestRenderSingleMember_stripe_unknown_status_stripe_unknown.html new file mode 100644 index 0000000..1d5cd62 --- /dev/null +++ b/modules/admin/fixtures/TestRenderSingleMember_stripe_unknown_status_stripe_unknown.html @@ -0,0 +1,19 @@ +Conway Makerspace System
Basic Info
Account created on Thu, Jun 15 2023
Login code
Fob ID Discord User ID
Discounts

The discounted rate is only applied during Stripe checkout. Applying a new discount will not modify existing memberships until the member re-subscribes.

Stripe !
Subscription status: unknown
View subscription in Stripe
Designations
Events
TimeEventDetails
\ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_badge_danger_badge_danger.html b/modules/admin/fixtures/TestTableCellRender_badge_danger_badge_danger.html new file mode 100644 index 0000000..02832cd --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_badge_danger_badge_danger.html @@ -0,0 +1 @@ +Inactive \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_badge_primary_badge_primary.html b/modules/admin/fixtures/TestTableCellRender_badge_primary_badge_primary.html new file mode 100644 index 0000000..5da2fb9 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_badge_primary_badge_primary.html @@ -0,0 +1 @@ +Active \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_badge_success_badge_success.html b/modules/admin/fixtures/TestTableCellRender_badge_success_badge_success.html new file mode 100644 index 0000000..f56b697 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_badge_success_badge_success.html @@ -0,0 +1 @@ +Ready \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_badge_warning_badge_warning.html b/modules/admin/fixtures/TestTableCellRender_badge_warning_badge_warning.html new file mode 100644 index 0000000..bbc06d9 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_badge_warning_badge_warning.html @@ -0,0 +1 @@ +Pending \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_empty_cell_empty.html b/modules/admin/fixtures/TestTableCellRender_empty_cell_empty.html new file mode 100644 index 0000000..31e7879 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_empty_cell_empty.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_self_link_button_button.html b/modules/admin/fixtures/TestTableCellRender_self_link_button_button.html new file mode 100644 index 0000000..7489f64 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_self_link_button_button.html @@ -0,0 +1 @@ +Edit \ No newline at end of file diff --git a/modules/admin/fixtures/TestTableCellRender_text_cell_text.html b/modules/admin/fixtures/TestTableCellRender_text_cell_text.html new file mode 100644 index 0000000..5016418 --- /dev/null +++ b/modules/admin/fixtures/TestTableCellRender_text_cell_text.html @@ -0,0 +1 @@ +Simple Text \ No newline at end of file diff --git a/modules/admin/member_test.go b/modules/admin/member_test.go new file mode 100644 index 0000000..47b4b6e --- /dev/null +++ b/modules/admin/member_test.go @@ -0,0 +1,304 @@ +package admin + +import ( + "testing" + "time" + + "github.com/TheLab-ms/conway/engine" + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderSingleMember(t *testing.T) { + // Helper function to create pointers + strPtr := func(s string) *string { return &s } + int64Ptr := func(i int64) *int64 { return &i } + float64Ptr := func(f float64) *float64 { return &f } + + // Helper to create a member with required fields set to safe defaults + createMember := func() *member { + return &member{ + FobID: int64Ptr(0), + RootFamilyEmail: strPtr(""), + DiscordUserID: "", + } + } + + // Base time for consistent test results + baseTime := time.Date(2023, 6, 15, 12, 0, 0, 0, time.UTC) + recentTime := baseTime.Add(-2 * time.Hour) + oldTime := baseTime.Add(-30 * 24 * time.Hour) + + tabs := []*navbarTab{ + {Title: "Members", Path: "/admin/members"}, + {Title: "Events", Path: "/admin/events"}, + } + + tests := []struct { + name string + member *member + events []*memberEvent + fixtureName string + description string + }{ + { + name: "minimal_member", + member: func() *member { + m := createMember() + m.ID = 123 + m.AccessStatus = "Ready" + m.Name = "John Doe" + m.Email = "john@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + return m + }(), + events: []*memberEvent{}, + fixtureName: "_minimal", + description: "Minimal member with basic required fields only", + }, + { + name: "complete_member_active", + member: func() *member { + m := createMember() + m.ID = 456 + m.AccessStatus = "Ready" + m.Name = "Jane Smith" + m.Email = "jane@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.AdminNotes = "VIP member, longtime contributor" + m.Leadership = true + m.NonBillable = false + m.FobID = int64Ptr(12345) + m.StripeSubID = strPtr("sub_1234567890") + m.StripeStatus = strPtr("active") + m.DiscountType = strPtr("military") + m.BillAnnually = true + m.FobLastSeen = &engine.LocalTime{Time: recentTime} + m.DiscordUserID = "123456789012345678" + return m + }(), + events: []*memberEvent{ + { + Created: baseTime.Add(-1 * time.Hour), + Event: "MembershipActivated", + Details: "Membership activated via Stripe", + }, + { + Created: baseTime.Add(-2 * time.Hour), + Event: "PaymentReceived", + Details: "Monthly payment processed", + }, + }, + fixtureName: "_complete_active", + description: "Complete active member with all fields populated", + }, + { + name: "inactive_member", + member: &member{ + ID: 789, + AccessStatus: "PaymentInactive", + Name: "Bob Johnson", + Email: "bob@example.com", + Confirmed: false, + Created: engine.LocalTime{Time: oldTime}, + AdminNotes: "Payment issues, needs follow up", + Leadership: false, + NonBillable: false, + FobID: int64Ptr(54321), + StripeSubID: nil, + StripeStatus: nil, + PaypalSubID: nil, + PaypalPrice: nil, + DiscountType: nil, + RootFamilyEmail: strPtr(""), + BillAnnually: false, + FobLastSeen: &engine.LocalTime{Time: oldTime}, + DiscordUserID: "", + }, + events: []*memberEvent{ + { + Created: baseTime.Add(-24 * time.Hour), + Event: "PaymentFailed", + Details: "Credit card declined", + }, + }, + fixtureName: "_inactive", + description: "Inactive member with payment issues", + }, + { + name: "stripe_unknown_status", + member: &member{ + ID: 101, + AccessStatus: "Ready", + Name: "Alice Wilson", + Email: "alice@example.com", + Confirmed: true, + Created: engine.LocalTime{Time: baseTime}, + FobID: int64Ptr(11111), + StripeSubID: strPtr("sub_unknown"), + StripeStatus: nil, // nil status should show "unknown" + PaypalSubID: nil, + PaypalPrice: nil, + DiscountType: nil, + RootFamilyEmail: strPtr(""), + BillAnnually: false, + FobLastSeen: nil, + DiscordUserID: "", + }, + events: []*memberEvent{}, + fixtureName: "_stripe_unknown", + description: "Member with Stripe subscription but unknown status", + }, + { + name: "stripe_empty_status", + member: func() *member { + m := createMember() + m.ID = 102 + m.AccessStatus = "Ready" + m.Name = "Charlie Brown" + m.Email = "charlie@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(22222) + m.StripeSubID = strPtr("sub_empty") + m.StripeStatus = strPtr("") // empty status should show "unknown" + return m + }(), + events: []*memberEvent{}, + fixtureName: "_stripe_empty", + description: "Member with empty Stripe status", + }, + { + name: "paypal_member", + member: func() *member { + m := createMember() + m.ID = 103 + m.AccessStatus = "Ready" + m.Name = "David Davis" + m.Email = "david@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(33333) + m.PaypalSubID = strPtr("I-PAYPAL123456") + m.PaypalPrice = float64Ptr(75.00) + return m + }(), + events: []*memberEvent{}, + fixtureName: "_paypal", + description: "Member with PayPal subscription", + }, + { + name: "family_discount", + member: func() *member { + m := createMember() + m.ID = 104 + m.AccessStatus = "Ready" + m.Name = "Emma Evans" + m.Email = "emma@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(44444) + m.DiscountType = strPtr("family") + m.RootFamilyEmail = strPtr("family-root@example.com") + return m + }(), + events: []*memberEvent{}, + fixtureName: "_family_discount", + description: "Member with family discount", + }, + { + name: "all_discounts", + member: func() *member { + m := createMember() + m.ID = 105 + m.AccessStatus = "Ready" + m.Name = "Frank Foster" + m.Email = "frank@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(55555) + m.DiscountType = strPtr("educator") + return m + }(), + events: []*memberEvent{}, + fixtureName: "_educator_discount", + description: "Member with educator discount", + }, + { + name: "non_billable_leadership", + member: func() *member { + m := createMember() + m.ID = 106 + m.AccessStatus = "Ready" + m.Name = "Grace Green" + m.Email = "grace@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.AdminNotes = "Board member, lifetime access" + m.Leadership = true + m.NonBillable = true + m.FobID = int64Ptr(66666) + return m + }(), + events: []*memberEvent{}, + fixtureName: "_leadership_nonbillable", + description: "Leadership member with non-billable status", + }, + { + name: "many_events", + member: func() *member { + m := createMember() + m.ID = 107 + m.AccessStatus = "Ready" + m.Name = "Henry Hill" + m.Email = "henry@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(77777) + return m + }(), + events: []*memberEvent{ + {Created: baseTime.Add(-1 * time.Hour), Event: "Event1", Details: "Details1"}, + {Created: baseTime.Add(-2 * time.Hour), Event: "Event2", Details: "Details2"}, + {Created: baseTime.Add(-3 * time.Hour), Event: "Event3", Details: "Details3"}, + {Created: baseTime.Add(-4 * time.Hour), Event: "Event4", Details: "Details4"}, + {Created: baseTime.Add(-5 * time.Hour), Event: "Event5", Details: "Details5"}, + {Created: baseTime.Add(-6 * time.Hour), Event: "Event6", Details: "Details6"}, + {Created: baseTime.Add(-7 * time.Hour), Event: "Event7", Details: "Details7"}, + {Created: baseTime.Add(-8 * time.Hour), Event: "Event8", Details: "Details8"}, + {Created: baseTime.Add(-9 * time.Hour), Event: "Event9", Details: "Details9"}, + {Created: baseTime.Add(-10 * time.Hour), Event: "Event10", Details: "Details10"}, + {Created: baseTime.Add(-11 * time.Hour), Event: "Event11", Details: "Details11"}, // This should trigger the "more than 10" message + }, + fixtureName: "_many_events", + description: "Member with more than 10 events", + }, + { + name: "stripe_inactive_no_sub", + member: func() *member { + m := createMember() + m.ID = 108 + m.AccessStatus = "Ready" + m.Name = "Ivy Johnson" + m.Email = "ivy@example.com" + m.Confirmed = true + m.Created = engine.LocalTime{Time: baseTime} + m.FobID = int64Ptr(88888) + m.StripeSubID = nil // No Stripe subscription + m.StripeStatus = strPtr("canceled") + return m + }(), + events: []*memberEvent{}, + fixtureName: "_stripe_no_sub", + description: "Member with no Stripe subscription ID", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderSingleMember(tabs, tt.member, tt.events) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginEmail_basic_email_basic.html b/modules/auth/fixtures/TestRenderLoginEmail_basic_email_basic.html new file mode 100644 index 0000000..d77ab21 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginEmail_basic_email_basic.html @@ -0,0 +1 @@ +

Here is your login code for TheLab Makerspace:

https://conway.thelab.ms/login

This link will expire in 5 minutes.

Please ignore this message if you did not request a login code from TheLab Makerspace.

\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginEmail_complex_callback_complex_callback.html b/modules/auth/fixtures/TestRenderLoginEmail_complex_callback_complex_callback.html new file mode 100644 index 0000000..2860ded --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginEmail_complex_callback_complex_callback.html @@ -0,0 +1 @@ +

Here is your login code for TheLab Makerspace:

https://conway.thelab.ms/login

This link will expire in 5 minutes.

Please ignore this message if you did not request a login code from TheLab Makerspace.

\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginEmail_empty_callback_empty_callback.html b/modules/auth/fixtures/TestRenderLoginEmail_empty_callback_empty_callback.html new file mode 100644 index 0000000..fb88417 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginEmail_empty_callback_empty_callback.html @@ -0,0 +1 @@ +

Here is your login code for TheLab Makerspace:

https://example.com/login

This link will expire in 5 minutes.

Please ignore this message if you did not request a login code from TheLab Makerspace.

\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginEmail_localhost_email_localhost.html b/modules/auth/fixtures/TestRenderLoginEmail_localhost_email_localhost.html new file mode 100644 index 0000000..44922e3 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginEmail_localhost_email_localhost.html @@ -0,0 +1 @@ +

Here is your login code for TheLab Makerspace:

http://localhost:8080/login

This link will expire in 5 minutes.

Please ignore this message if you did not request a login code from TheLab Makerspace.

\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginEmail_root_callback_root_callback.html b/modules/auth/fixtures/TestRenderLoginEmail_root_callback_root_callback.html new file mode 100644 index 0000000..6570d22 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginEmail_root_callback_root_callback.html @@ -0,0 +1 @@ +

Here is your login code for TheLab Makerspace:

https://conway.thelab.ms/login

This link will expire in 5 minutes.

Please ignore this message if you did not request a login code from TheLab Makerspace.

\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginPage_basic_login_basic.html b/modules/auth/fixtures/TestRenderLoginPage_basic_login_basic.html new file mode 100644 index 0000000..bfc47e4 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginPage_basic_login_basic.html @@ -0,0 +1 @@ +Conway Makerspace System
Login or Signup
\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginPage_complex_callback_complex_callback.html b/modules/auth/fixtures/TestRenderLoginPage_complex_callback_complex_callback.html new file mode 100644 index 0000000..f553fb1 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginPage_complex_callback_complex_callback.html @@ -0,0 +1 @@ +Conway Makerspace System
Login or Signup
\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginPage_empty_callback_empty_callback.html b/modules/auth/fixtures/TestRenderLoginPage_empty_callback_empty_callback.html new file mode 100644 index 0000000..879aaa2 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginPage_empty_callback_empty_callback.html @@ -0,0 +1 @@ +Conway Makerspace System
Login or Signup
\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginPage_root_callback_root_callback.html b/modules/auth/fixtures/TestRenderLoginPage_root_callback_root_callback.html new file mode 100644 index 0000000..69abc7d --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginPage_root_callback_root_callback.html @@ -0,0 +1 @@ +Conway Makerspace System
Login or Signup
\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginPage_with_turnstile_with_turnstile.html b/modules/auth/fixtures/TestRenderLoginPage_with_turnstile_with_turnstile.html new file mode 100644 index 0000000..e2604fd --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginPage_with_turnstile_with_turnstile.html @@ -0,0 +1 @@ +Conway Makerspace System
Login or Signup
\ No newline at end of file diff --git a/modules/auth/fixtures/TestRenderLoginSentPage.html b/modules/auth/fixtures/TestRenderLoginSentPage.html new file mode 100644 index 0000000..d7477b8 --- /dev/null +++ b/modules/auth/fixtures/TestRenderLoginSentPage.html @@ -0,0 +1 @@ +Conway Makerspace System
Email Sent

We sent a login link to the provided email address.

\ No newline at end of file diff --git a/modules/auth/login_test.go b/modules/auth/login_test.go new file mode 100644 index 0000000..d0d2074 --- /dev/null +++ b/modules/auth/login_test.go @@ -0,0 +1,129 @@ +package auth + +import ( + "net/url" + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderLoginPage(t *testing.T) { + tests := []struct { + name string + callbackURI string + tso *TurnstileOptions + fixtureName string + description string + }{ + { + name: "basic_login", + callbackURI: "/dashboard", + tso: nil, + fixtureName: "_basic", + description: "Basic login form without Turnstile", + }, + { + name: "with_turnstile", + callbackURI: "/admin", + tso: &TurnstileOptions{ + SiteKey: "0x4AAAAAAABkMYinukE8nzKr", + }, + fixtureName: "_with_turnstile", + description: "Login form with Turnstile CAPTCHA", + }, + { + name: "empty_callback", + callbackURI: "", + tso: nil, + fixtureName: "_empty_callback", + description: "Login form with empty callback URI", + }, + { + name: "complex_callback", + callbackURI: "/admin/members?search=test&page=2", + tso: &TurnstileOptions{ + SiteKey: "test-site-key-123", + }, + fixtureName: "_complex_callback", + description: "Login form with complex callback URI and Turnstile", + }, + { + name: "root_callback", + callbackURI: "/", + tso: nil, + fixtureName: "_root_callback", + description: "Login form with root callback", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderLoginPage(tt.callbackURI, tt.tso) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderLoginSentPage(t *testing.T) { + component := renderLoginSentPage() + snaptest.RenderSnapshotWithName(t, component, "") +} + +func TestRenderLoginEmail(t *testing.T) { + tests := []struct { + name string + self *url.URL + token string + callback string + fixtureName string + description string + }{ + { + name: "basic_email", + self: &url.URL{Scheme: "https", Host: "conway.thelab.ms"}, + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + callback: "/dashboard", + fixtureName: "_basic", + description: "Basic login email with token and callback", + }, + { + name: "localhost_email", + self: &url.URL{Scheme: "http", Host: "localhost:8080"}, + token: "test-token-123", + callback: "/admin", + fixtureName: "_localhost", + description: "Login email for localhost development", + }, + { + name: "empty_callback", + self: &url.URL{Scheme: "https", Host: "example.com"}, + token: "short-token", + callback: "", + fixtureName: "_empty_callback", + description: "Login email with empty callback", + }, + { + name: "complex_callback", + self: &url.URL{Scheme: "https", Host: "conway.thelab.ms"}, + token: "complex-token-with-special-chars", + callback: "/members?filter=active&sort=name", + fixtureName: "_complex_callback", + description: "Login email with complex callback URI", + }, + { + name: "root_callback", + self: &url.URL{Scheme: "https", Host: "conway.thelab.ms"}, + token: "root-token", + callback: "/", + fixtureName: "_root_callback", + description: "Login email with root callback", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderLoginEmail(tt.self, tt.token, tt.callback) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/bootstrap/bootstrap_test.go b/modules/bootstrap/bootstrap_test.go new file mode 100644 index 0000000..017dce9 --- /dev/null +++ b/modules/bootstrap/bootstrap_test.go @@ -0,0 +1,67 @@ +package bootstrap + +import ( + "context" + "io" + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" + "github.com/a-h/templ" +) + +// mockComponent creates a simple test component for testing bootstrap layouts +func mockComponent() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + _, err = w.Write([]byte(`

Test Content

This is test content.

`)) + return err + }) +} + +func TestView(t *testing.T) { + // Test View() by creating a component that uses it with mock content + component := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return View().Render(templ.WithChildren(ctx, mockComponent()), w) + }) + snaptest.RenderSnapshotWithName(t, component, "") +} + +func TestDarkmodeView(t *testing.T) { + // Test DarkmodeView() by creating a component that uses it with mock content + component := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return DarkmodeView().Render(templ.WithChildren(ctx, mockComponent()), w) + }) + snaptest.RenderSnapshotWithName(t, component, "") +} + +func TestViewWithTheme(t *testing.T) { + tests := []struct { + name string + theme string + fixtureName string + }{ + { + name: "empty_theme", + theme: "", + fixtureName: "_empty_theme", + }, + { + name: "dark_theme", + theme: "dark", + fixtureName: "_dark_theme", + }, + { + name: "custom_theme", + theme: "custom", + fixtureName: "_custom_theme", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return view(tt.theme).Render(templ.WithChildren(ctx, mockComponent()), w) + }) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/bootstrap/fixtures/TestDarkmodeView.html b/modules/bootstrap/fixtures/TestDarkmodeView.html new file mode 100644 index 0000000..ab62d68 --- /dev/null +++ b/modules/bootstrap/fixtures/TestDarkmodeView.html @@ -0,0 +1 @@ +Conway Makerspace System

Test Content

This is test content.

\ No newline at end of file diff --git a/modules/bootstrap/fixtures/TestView.html b/modules/bootstrap/fixtures/TestView.html new file mode 100644 index 0000000..73a11b5 --- /dev/null +++ b/modules/bootstrap/fixtures/TestView.html @@ -0,0 +1 @@ +Conway Makerspace System

Test Content

This is test content.

\ No newline at end of file diff --git a/modules/bootstrap/fixtures/TestViewWithTheme_custom_theme_custom_theme.html b/modules/bootstrap/fixtures/TestViewWithTheme_custom_theme_custom_theme.html new file mode 100644 index 0000000..87dd4f5 --- /dev/null +++ b/modules/bootstrap/fixtures/TestViewWithTheme_custom_theme_custom_theme.html @@ -0,0 +1 @@ +Conway Makerspace System

Test Content

This is test content.

\ No newline at end of file diff --git a/modules/bootstrap/fixtures/TestViewWithTheme_dark_theme_dark_theme.html b/modules/bootstrap/fixtures/TestViewWithTheme_dark_theme_dark_theme.html new file mode 100644 index 0000000..ab62d68 --- /dev/null +++ b/modules/bootstrap/fixtures/TestViewWithTheme_dark_theme_dark_theme.html @@ -0,0 +1 @@ +Conway Makerspace System

Test Content

This is test content.

\ No newline at end of file diff --git a/modules/bootstrap/fixtures/TestViewWithTheme_empty_theme_empty_theme.html b/modules/bootstrap/fixtures/TestViewWithTheme_empty_theme_empty_theme.html new file mode 100644 index 0000000..73a11b5 --- /dev/null +++ b/modules/bootstrap/fixtures/TestViewWithTheme_empty_theme_empty_theme.html @@ -0,0 +1 @@ +Conway Makerspace System

Test Content

This is test content.

\ No newline at end of file diff --git a/modules/kiosk/fixtures/TestRenderKioskLargeQR_large_qr.html b/modules/kiosk/fixtures/TestRenderKioskLargeQR_large_qr.html new file mode 100644 index 0000000..2310e37 --- /dev/null +++ b/modules/kiosk/fixtures/TestRenderKioskLargeQR_large_qr.html @@ -0,0 +1,50 @@ +Conway Makerspace System

Link to Your Account

Scan the QR from your device to link the key fob to your account.
Done
\ No newline at end of file diff --git a/modules/kiosk/fixtures/TestRenderKiosk_empty_qr_bytes_empty_qr.html b/modules/kiosk/fixtures/TestRenderKiosk_empty_qr_bytes_empty_qr.html new file mode 100644 index 0000000..fde4033 --- /dev/null +++ b/modules/kiosk/fixtures/TestRenderKiosk_empty_qr_bytes_empty_qr.html @@ -0,0 +1,50 @@ +Conway Makerspace System

Link to Your Account

Scan the QR from your device to link the key fob to your account.
Done
\ No newline at end of file diff --git a/modules/kiosk/fixtures/TestRenderKiosk_no_qr_code_no_qr.html b/modules/kiosk/fixtures/TestRenderKiosk_no_qr_code_no_qr.html new file mode 100644 index 0000000..f338568 --- /dev/null +++ b/modules/kiosk/fixtures/TestRenderKiosk_no_qr_code_no_qr.html @@ -0,0 +1,50 @@ +Conway Makerspace System

Welcome to TheLab

How To Join Sign Waiver
Scan a key fob any time to link it to your account.
\ No newline at end of file diff --git a/modules/kiosk/fixtures/TestRenderKiosk_with_qr_code_with_qr.html b/modules/kiosk/fixtures/TestRenderKiosk_with_qr_code_with_qr.html new file mode 100644 index 0000000..7d3dfb7 --- /dev/null +++ b/modules/kiosk/fixtures/TestRenderKiosk_with_qr_code_with_qr.html @@ -0,0 +1,50 @@ +Conway Makerspace System

Link to Your Account

Scan the QR from your device to link the key fob to your account.
Done
\ No newline at end of file diff --git a/modules/kiosk/fixtures/TestRenderOffsiteError.html b/modules/kiosk/fixtures/TestRenderOffsiteError.html new file mode 100644 index 0000000..80c3b3f --- /dev/null +++ b/modules/kiosk/fixtures/TestRenderOffsiteError.html @@ -0,0 +1 @@ +Conway Makerspace System

Uh oh

You need to be at the physical makerspace to assign keyfobs.
\ No newline at end of file diff --git a/modules/kiosk/kiosk_test.go b/modules/kiosk/kiosk_test.go new file mode 100644 index 0000000..f4964d8 --- /dev/null +++ b/modules/kiosk/kiosk_test.go @@ -0,0 +1,61 @@ +package kiosk + +import ( + "encoding/base64" + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderOffsiteError(t *testing.T) { + component := renderOffsiteError() + snaptest.RenderSnapshotWithName(t, component, "") +} + +func TestRenderKiosk(t *testing.T) { + tests := []struct { + name string + qrImg []byte + fixtureName string + description string + }{ + { + name: "no_qr_code", + qrImg: nil, + fixtureName: "_no_qr", + description: "Test kiosk view with no QR code (welcome screen)", + }, + { + name: "with_qr_code", + qrImg: []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="), // 1x1 transparent PNG in base64 + fixtureName: "_with_qr", + description: "Test kiosk view with QR code for fob linking", + }, + { + name: "empty_qr_bytes", + qrImg: []byte{}, + fixtureName: "_empty_qr", + description: "Test kiosk view with empty byte slice (should show QR screen)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderKiosk(tt.qrImg) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderKioskLargeQR(t *testing.T) { + // Test with a larger QR code to ensure proper rendering + largePNG := make([]byte, 1024) + for i := range largePNG { + largePNG[i] = byte(i % 256) + } + encodedPNG := make([]byte, base64.StdEncoding.EncodedLen(len(largePNG))) + base64.StdEncoding.Encode(encodedPNG, largePNG) + + component := renderKiosk(encodedPNG) + snaptest.RenderSnapshotWithName(t, component, "_large_qr") +} \ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_available_printers_available.html b/modules/machines/fixtures/TestRenderMachines_available_printers_available.html new file mode 100644 index 0000000..aeac863 --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_available_printers_available.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
Printer1Available
Printer2AvailableE001
\ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_busy_printers_busy.html b/modules/machines/fixtures/TestRenderMachines_busy_printers_busy.html new file mode 100644 index 0000000..c234c69 --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_busy_printers_busy.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
BusyPrinter1In Use2h0m0s
BusyPrinter2In Use2h30m0sW001
\ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_empty_printers_empty.html b/modules/machines/fixtures/TestRenderMachines_empty_printers_empty.html new file mode 100644 index 0000000..5f48c0d --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_empty_printers_empty.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
\ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_mixed_status_mixed.html b/modules/machines/fixtures/TestRenderMachines_mixed_status_mixed.html new file mode 100644 index 0000000..e1e33a4 --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_mixed_status_mixed.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
AvailablePrinterAvailable
BusyPrinterIn Use2h0m0s
ErrorPrinterAvailableE502
BusyErrorPrinterIn Use3h0m0sW100
\ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_overdue_job_overdue.html b/modules/machines/fixtures/TestRenderMachines_overdue_job_overdue.html new file mode 100644 index 0000000..8b2b878 --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_overdue_job_overdue.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
OverduePrinterIn Use-1h0m0s
\ No newline at end of file diff --git a/modules/machines/fixtures/TestRenderMachines_single_printer_single.html b/modules/machines/fixtures/TestRenderMachines_single_printer_single.html new file mode 100644 index 0000000..84c4063 --- /dev/null +++ b/modules/machines/fixtures/TestRenderMachines_single_printer_single.html @@ -0,0 +1 @@ +Conway Makerspace System

Printers

PrinterStatusPrint Completion EstimateError Code
SinglePrinterIn Use2h15m0sI001
\ No newline at end of file diff --git a/modules/machines/machines_test.go b/modules/machines/machines_test.go new file mode 100644 index 0000000..f24b093 --- /dev/null +++ b/modules/machines/machines_test.go @@ -0,0 +1,122 @@ +package machines + +import ( + "testing" + "time" + + "github.com/TheLab-ms/conway/engine" + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderMachines(t *testing.T) { + // Create test time values + now := time.Now() + future := now.Add(2 * time.Hour) + past := now.Add(-1 * time.Hour) + + tests := []struct { + name string + printers []*printerStatus + fixtureName string + description string + }{ + { + name: "empty_printers", + printers: []*printerStatus{}, + fixtureName: "_empty", + description: "No printers available", + }, + { + name: "available_printers", + printers: []*printerStatus{ + { + Name: "Printer1", + JobFinishedAt: nil, + ErrorCode: "", + }, + { + Name: "Printer2", + JobFinishedAt: nil, + ErrorCode: "E001", + }, + }, + fixtureName: "_available", + description: "Available printers, some with error codes", + }, + { + name: "busy_printers", + printers: []*printerStatus{ + { + Name: "BusyPrinter1", + JobFinishedAt: &engine.LocalTime{Time: future}, + ErrorCode: "", + }, + { + Name: "BusyPrinter2", + JobFinishedAt: &engine.LocalTime{Time: future.Add(30 * time.Minute)}, + ErrorCode: "W001", + }, + }, + fixtureName: "_busy", + description: "Printers currently printing jobs", + }, + { + name: "mixed_status", + printers: []*printerStatus{ + { + Name: "AvailablePrinter", + JobFinishedAt: nil, + ErrorCode: "", + }, + { + Name: "BusyPrinter", + JobFinishedAt: &engine.LocalTime{Time: future}, + ErrorCode: "", + }, + { + Name: "ErrorPrinter", + JobFinishedAt: nil, + ErrorCode: "E502", + }, + { + Name: "BusyErrorPrinter", + JobFinishedAt: &engine.LocalTime{Time: future.Add(1 * time.Hour)}, + ErrorCode: "W100", + }, + }, + fixtureName: "_mixed", + description: "Mixed printer statuses - available, busy, with/without errors", + }, + { + name: "overdue_job", + printers: []*printerStatus{ + { + Name: "OverduePrinter", + JobFinishedAt: &engine.LocalTime{Time: past}, + ErrorCode: "", + }, + }, + fixtureName: "_overdue", + description: "Printer with overdue job completion time", + }, + { + name: "single_printer", + printers: []*printerStatus{ + { + Name: "SinglePrinter", + JobFinishedAt: &engine.LocalTime{Time: future.Add(15 * time.Minute)}, + ErrorCode: "I001", + }, + }, + fixtureName: "_single", + description: "Single printer with job and info code", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderMachines(tt.printers) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_active_member_active.html b/modules/members/fixtures/TestRenderMember_active_member_active.html new file mode 100644 index 0000000..b1cf228 --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_active_member_active.html @@ -0,0 +1 @@ +Conway Makerspace System

Active Member

Your key fob is active!
Manage Payment Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_family_inactive_family_inactive.html b/modules/members/fixtures/TestRenderMember_family_inactive_family_inactive.html new file mode 100644 index 0000000..f332a85 --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_family_inactive_family_inactive.html @@ -0,0 +1 @@ +Conway Makerspace System

Family Member Inactive

Your membership is part of a family discount that is no longer valid due to the "root" member's account becoming inactive.
Manage Payment Link Discord Account Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_member_no_discord_no_discord.html b/modules/members/fixtures/TestRenderMember_member_no_discord_no_discord.html new file mode 100644 index 0000000..99b8c1d --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_member_no_discord_no_discord.html @@ -0,0 +1 @@ +Conway Makerspace System

Active Member

Your key fob is active!
Manage Payment Link Discord Account Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_missing_keyfob_missing_keyfob.html b/modules/members/fixtures/TestRenderMember_missing_keyfob_missing_keyfob.html new file mode 100644 index 0000000..f705cbb --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_missing_keyfob_missing_keyfob.html @@ -0,0 +1 @@ +Conway Makerspace System

Pick Up Your Key

You need an RFID key fob to access the space, but your account isn't currently linked to one. If you're at the space, you can add one using the kiosk near the front door.
Manage Payment Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_missing_waiver_missing_waiver.html b/modules/members/fixtures/TestRenderMember_missing_waiver_missing_waiver.html new file mode 100644 index 0000000..e8a7c93 --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_missing_waiver_missing_waiver.html @@ -0,0 +1 @@ +Conway Makerspace System

Missing Liability Waiver

You will need to sign our liability waiver before entering the space.

Sign Waiver
Manage Payment Link Discord Account Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMember_payment_inactive_payment_inactive.html b/modules/members/fixtures/TestRenderMember_payment_inactive_payment_inactive.html new file mode 100644 index 0000000..913f3b5 --- /dev/null +++ b/modules/members/fixtures/TestRenderMember_payment_inactive_payment_inactive.html @@ -0,0 +1 @@ +Conway Makerspace System

Missing Billing Information

Use the button below to set up payment with our secure payment processor.
Manage Payment Logout
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_default_active_default_status.html b/modules/members/fixtures/TestRenderMembershipStatus_default_active_default_status.html new file mode 100644 index 0000000..acb8206 --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_default_active_default_status.html @@ -0,0 +1 @@ +

Active Member

Your key fob is active!
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_family_inactive_status_family_status.html b/modules/members/fixtures/TestRenderMembershipStatus_family_inactive_status_family_status.html new file mode 100644 index 0000000..4ff1ad9 --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_family_inactive_status_family_status.html @@ -0,0 +1 @@ +

Family Member Inactive

Your membership is part of a family discount that is no longer valid due to the "root" member's account becoming inactive.
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_missing_keyfob_status_keyfob_status.html b/modules/members/fixtures/TestRenderMembershipStatus_missing_keyfob_status_keyfob_status.html new file mode 100644 index 0000000..c5a8e2b --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_missing_keyfob_status_keyfob_status.html @@ -0,0 +1 @@ +

Pick Up Your Key

You need an RFID key fob to access the space, but your account isn't currently linked to one. If you're at the space, you can add one using the kiosk near the front door.
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_missing_waiver_status_waiver_status.html b/modules/members/fixtures/TestRenderMembershipStatus_missing_waiver_status_waiver_status.html new file mode 100644 index 0000000..9fe2717 --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_missing_waiver_status_waiver_status.html @@ -0,0 +1 @@ +

Missing Liability Waiver

You will need to sign our liability waiver before entering the space.

Sign Waiver
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_payment_inactive_status_payment_status.html b/modules/members/fixtures/TestRenderMembershipStatus_payment_inactive_status_payment_status.html new file mode 100644 index 0000000..8ea2f20 --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_payment_inactive_status_payment_status.html @@ -0,0 +1 @@ +

Missing Billing Information

Use the button below to set up payment with our secure payment processor.
\ No newline at end of file diff --git a/modules/members/fixtures/TestRenderMembershipStatus_unknown_status_unknown_status.html b/modules/members/fixtures/TestRenderMembershipStatus_unknown_status_unknown_status.html new file mode 100644 index 0000000..acb8206 --- /dev/null +++ b/modules/members/fixtures/TestRenderMembershipStatus_unknown_status_unknown_status.html @@ -0,0 +1 @@ +

Active Member

Your key fob is active!
\ No newline at end of file diff --git a/modules/members/member_test.go b/modules/members/member_test.go new file mode 100644 index 0000000..b31ae71 --- /dev/null +++ b/modules/members/member_test.go @@ -0,0 +1,167 @@ +package members + +import ( + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderMember(t *testing.T) { + tests := []struct { + name string + member *member + fixtureName string + description string + }{ + { + name: "active_member", + member: &member{ + ID: 123, + AccessStatus: "Ready", + DiscordLinked: true, + Email: "user@example.com", + }, + fixtureName: "_active", + description: "Active member with Discord linked", + }, + { + name: "member_no_discord", + member: &member{ + ID: 456, + AccessStatus: "Ready", + DiscordLinked: false, + Email: "user2@example.com", + }, + fixtureName: "_no_discord", + description: "Active member without Discord linked", + }, + { + name: "missing_waiver", + member: &member{ + ID: 789, + AccessStatus: "MissingWaiver", + DiscordLinked: false, + Email: "user3@example.com", + }, + fixtureName: "_missing_waiver", + description: "Member missing liability waiver", + }, + { + name: "missing_keyfob", + member: &member{ + ID: 101, + AccessStatus: "MissingKeyFob", + DiscordLinked: true, + Email: "user4@example.com", + }, + fixtureName: "_missing_keyfob", + description: "Member missing key fob", + }, + { + name: "family_inactive", + member: &member{ + ID: 102, + AccessStatus: "FamilyInactive", + DiscordLinked: false, + Email: "family@example.com", + }, + fixtureName: "_family_inactive", + description: "Family member with inactive root account", + }, + { + name: "payment_inactive", + member: &member{ + ID: 103, + AccessStatus: "PaymentInactive", + DiscordLinked: true, + Email: "payment@example.com", + }, + fixtureName: "_payment_inactive", + description: "Member with inactive payment", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderMember(tt.member) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} + +func TestRenderMembershipStatus(t *testing.T) { + tests := []struct { + name string + member *member + fixtureName string + description string + }{ + { + name: "default_active", + member: &member{ + ID: 123, + AccessStatus: "Ready", + Email: "user@example.com", + }, + fixtureName: "_default_status", + description: "Default case - active member", + }, + { + name: "unknown_status", + member: &member{ + ID: 123, + AccessStatus: "SomeUnknownStatus", + Email: "user@example.com", + }, + fixtureName: "_unknown_status", + description: "Unknown status should use default case", + }, + { + name: "missing_waiver_status", + member: &member{ + ID: 456, + AccessStatus: "MissingWaiver", + Email: "waiver@example.com", + }, + fixtureName: "_waiver_status", + description: "Missing waiver status with link to waiver form", + }, + { + name: "missing_keyfob_status", + member: &member{ + ID: 789, + AccessStatus: "MissingKeyFob", + Email: "keyfob@example.com", + }, + fixtureName: "_keyfob_status", + description: "Missing key fob status", + }, + { + name: "family_inactive_status", + member: &member{ + ID: 101, + AccessStatus: "FamilyInactive", + Email: "family@example.com", + }, + fixtureName: "_family_status", + description: "Family inactive status", + }, + { + name: "payment_inactive_status", + member: &member{ + ID: 102, + AccessStatus: "PaymentInactive", + Email: "payment@example.com", + }, + fixtureName: "_payment_status", + description: "Payment inactive status", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderMembershipStatus(tt.member) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_signed_special_chars_signed_special_chars.html b/modules/waiver/fixtures/TestRenderWaiver_signed_special_chars_signed_special_chars.html new file mode 100644 index 0000000..4b0f634 --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_signed_special_chars_signed_special_chars.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_complex_redirect_signed_complex_redirect.html b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_complex_redirect_signed_complex_redirect.html new file mode 100644 index 0000000..fe61a89 --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_complex_redirect_signed_complex_redirect.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_no_redirect_signed_no_redirect.html b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_no_redirect_signed_no_redirect.html new file mode 100644 index 0000000..853cb8f --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_no_redirect_signed_no_redirect.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_with_redirect_signed_with_redirect.html b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_with_redirect_signed_with_redirect.html new file mode 100644 index 0000000..859a606 --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_signed_waiver_with_redirect_signed_with_redirect.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_unsigned_special_chars_unsigned_special_chars.html b/modules/waiver/fixtures/TestRenderWaiver_unsigned_special_chars_unsigned_special_chars.html new file mode 100644 index 0000000..1343c0e --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_unsigned_special_chars_unsigned_special_chars.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_empty_unsigned_empty.html b/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_empty_unsigned_empty.html new file mode 100644 index 0000000..e4ac9e2 --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_empty_unsigned_empty.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_with_data_unsigned_with_data.html b/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_with_data_unsigned_with_data.html new file mode 100644 index 0000000..13f31d4 --- /dev/null +++ b/modules/waiver/fixtures/TestRenderWaiver_unsigned_waiver_with_data_unsigned_with_data.html @@ -0,0 +1,6 @@ +Conway Makerspace System

TheLab Liability Waiver

I agree and acknowledge as follows:

1. I WAIVE ANY AND ALL RIGHTS OF RECOVERY, CLAIM, ACTION OR CAUSE OF ACTION AGAINST THELAB.MS FOR ANY INJURY OR DAMAGE THAT MAY OCCUR, REGARDLESS OF CAUSE OR ORIGIN, INCLUDING NEGLIGENCE AND GROSS NEGLIGENCE.

2. I also understand that I am personally responsible for my safety and actions and that I will follow all safety instructions and signage while at TheLab.ms.

3. I affirm that I am at least 18 years of age and mentally competent to sign this liability waiver.

\ No newline at end of file diff --git a/modules/waiver/waiver_test.go b/modules/waiver/waiver_test.go new file mode 100644 index 0000000..1ebfcb2 --- /dev/null +++ b/modules/waiver/waiver_test.go @@ -0,0 +1,90 @@ +package waiver + +import ( + "testing" + + snaptest "github.com/TheLab-ms/conway/internal/testing" +) + +func TestRenderWaiver(t *testing.T) { + tests := []struct { + name string + signed bool + name_param string + email string + redirect string + fixtureName string + description string + }{ + { + name: "unsigned_waiver_empty", + signed: false, + name_param: "", + email: "", + redirect: "", + fixtureName: "_unsigned_empty", + description: "Empty waiver form for new user", + }, + { + name: "unsigned_waiver_with_data", + signed: false, + name_param: "John Doe", + email: "john@example.com", + redirect: "/dashboard", + fixtureName: "_unsigned_with_data", + description: "Waiver form pre-filled with user data", + }, + { + name: "signed_waiver_no_redirect", + signed: true, + name_param: "Jane Smith", + email: "jane@example.com", + redirect: "", + fixtureName: "_signed_no_redirect", + description: "Successfully signed waiver without redirect", + }, + { + name: "signed_waiver_with_redirect", + signed: true, + name_param: "Bob Johnson", + email: "bob@example.com", + redirect: "/kiosk", + fixtureName: "_signed_with_redirect", + description: "Successfully signed waiver with redirect to kiosk", + }, + { + name: "signed_waiver_complex_redirect", + signed: true, + name_param: "Alice Wilson", + email: "alice@example.com", + redirect: "/admin/members?filter=new", + fixtureName: "_signed_complex_redirect", + description: "Successfully signed waiver with complex redirect URL", + }, + { + name: "unsigned_special_chars", + signed: false, + name_param: "José García-López", + email: "jose.garcia+test@example.com", + redirect: "/test?param=value&other=123", + fixtureName: "_unsigned_special_chars", + description: "Waiver form with special characters and complex redirect", + }, + { + name: "signed_special_chars", + signed: true, + name_param: "Marie-Claire O'Connor", + email: "marie.claire@example.co.uk", + redirect: "/success?msg=waiver_complete", + fixtureName: "_signed_special_chars", + description: "Signed waiver with special characters in name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := renderWaiver(tt.signed, tt.name_param, tt.email, tt.redirect) + snaptest.RenderSnapshotWithName(t, component, tt.fixtureName) + }) + } +} \ No newline at end of file