Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
428efe2
test(phase1): add unit tests for config and gitlab check
obcode Apr 23, 2026
b1cd067
test(phase2): add gitlab API contract tests with httptest
obcode Apr 23, 2026
139d943
refactor(phase3): add injectable exit/panic seams for gitlab flows
obcode Apr 23, 2026
b66bb31
test(phase4): add opt-in GitLab testcontainer integration test
obcode Apr 23, 2026
06fa0b1
ci(phase5): split fast and integration test jobs
obcode Apr 23, 2026
add0dfa
ci: fix workflow syntax and add coverage job
obcode Apr 23, 2026
7edf6d8
ci: add PR coverage comment and fix workflow indentation
obcode Apr 23, 2026
706072c
test: add broad happy-path edge and error coverage
obcode Apr 23, 2026
07def9b
test: raise coverage for cmd, git clone, and gitlab issues
obcode Apr 23, 2026
f59fe57
test: add integration tests for Archive, Delete, ProtectToBranch, Set…
obcode Apr 23, 2026
296bb4e
docs: document integration test commands and update CI to run all int…
obcode Apr 23, 2026
27fa2c6
docs: run only TestIntegration_* in integration commands
obcode Apr 23, 2026
53db32d
test: use strong root password for GitLab integration container
obcode Apr 23, 2026
865aa38
test: use random non-dictionary root password for GitLab container
obcode Apr 23, 2026
8ae8319
test: strip Docker multiplexed stream headers from exec output in cre…
obcode Apr 23, 2026
e27063c
fix: avoid nil startercode dereference in archive logging
obcode Apr 23, 2026
8b9629c
docs: clarify integration test opt-in process and environment variabl…
obcode Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 127 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,64 +1,135 @@
name: ci

on:
push:
branches:
- "**"
push:
branches:
- "**"
pull_request:
workflow_dispatch:
inputs:
run_integration:
description: "Run integration tests with GitLab Testcontainers"
required: false
default: false
type: boolean

permissions:
contents: write
contents: write
pull-requests: write

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install golangci-lint
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Run golangci-lint
run: golangci-lint run --timeout=10m
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install golangci-lint
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Run golangci-lint
run: golangci-lint run --timeout=10m

test-fast:
name: test (fast)
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Download dependencies
run: go mod download
- name: Run fast tests
run: go test -v ./...

coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Download dependencies
run: go mod download
- name: Generate coverage
id: coverage-output
run: |
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out | tee coverage.txt
{
echo 'coverage_text<<EOF_TEXT'
cat coverage.txt
echo EOF_TEXT
} >> "$GITHUB_OUTPUT"
echo "## Coverage" >> "$GITHUB_STEP_SUMMARY"
echo '```text' >> "$GITHUB_STEP_SUMMARY"
cat coverage.txt >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: go-coverage
path: |
coverage.out
coverage.txt
- name: Comment coverage on PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: coverage
message: |
## Coverage

```text
${{ steps.coverage-output.outputs.coverage_text }}
```

test:
name: test
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
id: go
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Test
run: |
go test -v ./...
test-integration:
name: test (integration)
runs-on: ubuntu-latest
timeout-minutes: 90
if: >-
github.ref == 'refs/heads/main' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_integration == 'true')
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go from go.mod
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Download dependencies
run: go mod download
- name: Run integration tests
env:
GLABS_RUN_GITLAB_TC: "1"
run: |
go test -tags=integration ./gitlab/... -count=1 -v -run '^TestIntegration_'

release:
runs-on: ubuntu-latest
needs:
- golangci
- test
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: go-semantic-release/action@v1.0.0
with:
changelog-generator-opt: "emojis=true"
allow-initial-development-versions: true
hooks: goreleaser
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release:
runs-on: ubuntu-latest
needs:
- golangci
- test-fast
- coverage
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: go-semantic-release/action@v1.0.0
with:
changelog-generator-opt: "emojis=true"
allow-initial-development-versions: true
hooks: goreleaser
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,35 @@ glabs report <course> <assignment> [--html|--json]

Issues and pull requests are welcome.

## Testing

Default unit and contract tests:

```sh
go test ./...
```

Integration tests with GitLab Testcontainers (opt-in):

```sh
# Group/project lifecycle (createGroup, generateProject, …)
GLABS_RUN_GITLAB_TC=1 go test -tags=integration -v -count=1 ./gitlab/... -run TestIntegration_GitLab_GroupAndProjectLifecycle

# Archive, Delete, ProtectToBranch, Setaccess end-to-end
GLABS_RUN_GITLAB_TC=1 go test -tags=integration -v -count=1 ./gitlab/... -run TestIntegration_GitLab_Operations

# Run all integration tests at once
GLABS_RUN_GITLAB_TC=1 go test -tags=integration -v -count=1 ./gitlab/... -run '^TestIntegration_'
```

Notes:

- Integration tests are intentionally opt-in because starting GitLab CE in a container takes 5–25 minutes.
- `GLABS_RUN_GITLAB_TC` means: run GitLab Testcontainer tests.
- Set `GLABS_RUN_GITLAB_TC=1` to enable them; without it the tests are skipped automatically.
- Example: `GLABS_RUN_GITLAB_TC=0` (or variable unset) keeps integration tests disabled.
- In CI, trigger them via the `run_integration` workflow dispatch input (dedicated `test-integration` job).

## License

MIT, see [LICENSE](LICENSE).
85 changes: 85 additions & 0 deletions cmd/smoke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"os"
"path/filepath"
"strings"
"testing"
)

func resetReportGlobals(t *testing.T) {
t.Helper()
oldHTML := Html
oldJSON := Json
oldTemplate := Template
oldExportTemplate := ExportTemplate
oldOutput := OutPut
t.Cleanup(func() {
Html = oldHTML
Json = oldJSON
Template = oldTemplate
ExportTemplate = oldExportTemplate
OutPut = oldOutput
})
}

func TestRootCommand_HasSubcommands(t *testing.T) {
if len(rootCmd.Commands()) == 0 {
t.Fatal("expected root command to have subcommands")
}
}

func TestReportCmd_ArgsRequireTwoArgs(t *testing.T) {
resetReportGlobals(t)
ExportTemplate = false

err := reportCmd.Args(reportCmd, []string{"course-only"})
if err == nil {
t.Fatal("expected args validation error")
}
}

func TestReportCmd_ArgsAllowNoArgsWhenExportTemplate(t *testing.T) {
resetReportGlobals(t)
ExportTemplate = true

if err := reportCmd.Args(reportCmd, nil); err != nil {
t.Fatalf("Args() unexpected error: %v", err)
}
}

func TestReportCmd_RunPanicsForHtmlAndJSONTogether(t *testing.T) {
resetReportGlobals(t)
Html = true
Json = true

defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic when --html and --json are both set")
}
}()

reportCmd.Run(reportCmd, []string{"mpd", "blatt01"})
}

func TestReportCmd_ExportDefaultTemplateToFile(t *testing.T) {
resetReportGlobals(t)
ExportTemplate = true
Html = true

out := filepath.Join(t.TempDir(), "default-report-template.html")
OutPut = out

reportCmd.Run(reportCmd, nil)

data, err := os.ReadFile(out)
if err != nil {
t.Fatalf("reading template output file failed: %v", err)
}
if len(data) == 0 {
t.Fatal("expected template output file to be non-empty")
}
if !strings.Contains(string(data), "<html") {
t.Fatalf("expected HTML template content, got: %q", string(data))
}
}
23 changes: 23 additions & 0 deletions config/accesslevel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import "testing"

func TestAccessLevelString(t *testing.T) {
tests := []struct {
level AccessLevel
want string
}{
{Guest, "guest"},
{Reporter, "reporter"},
{Developer, "developer"},
{Maintainer, "maintainer"},
{AccessLevel(99), "maintainer"}, // default: anything != 10/20/30 returns maintainer
}

for _, tc := range tests {
got := tc.level.String()
if got != tc.want {
t.Errorf("AccessLevel(%d).String() = %q, want %q", tc.level, got, tc.want)
}
}
}
Loading
Loading