Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- run: go vet ./...
- run: go test -race -count=1 -coverprofile=coverage.out ./...
- run: go tool cover -func=coverage.out | tail -20
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ clean:

check:
go vet ./...
go test ./...
go test -race ./...
28 changes: 28 additions & 0 deletions api/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package api

import "testing"

func TestSetRegion(t *testing.T) {
orig := region
t.Cleanup(func() { region = orig })

cases := []struct {
name string
input string
want string
}{
{name: "empty stays empty", input: "", want: ""},
{name: "US is normalized to empty", input: "US", want: ""},
{name: "EMEA is preserved", input: "EMEA", want: "EMEA"},
{name: "AUS is preserved", input: "AUS", want: "AUS"},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
SetRegion(tc.input)
if region != tc.want {
t.Errorf("SetRegion(%q): region = %q, want %q", tc.input, region, tc.want)
}
})
}
}
89 changes: 89 additions & 0 deletions api/details_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package api

import (
"testing"
"time"
)

func TestParseTime(t *testing.T) {
mustParse := func(layout, s string) time.Time {
t.Helper()
v, err := time.Parse(layout, s)
if err != nil {
t.Fatalf("setup: parsing %q with %q: %v", s, layout, err)
}
return v
}

cases := []struct {
name string
input string
wantZero bool
want time.Time
}{
{
name: "empty string returns zero time",
input: "",
wantZero: true,
},
{
name: "RFC3339 UTC",
input: "2024-01-15T10:30:45Z",
want: mustParse(time.RFC3339, "2024-01-15T10:30:45Z"),
},
{
name: "fractional Z fallback",
input: "2024-01-15T10:30:45.123Z",
want: mustParse("2006-01-02T15:04:05.000Z", "2024-01-15T10:30:45.123Z"),
},
{
name: "RFC3339 with positive offset",
input: "2024-01-15T10:30:45+02:00",
want: mustParse(time.RFC3339, "2024-01-15T10:30:45+02:00"),
},
{
name: "garbage returns zero time",
input: "garbage",
wantZero: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := parseTime(tc.input)
if tc.wantZero {
if !got.IsZero() {
t.Errorf("parseTime(%q) = %v, want zero time", tc.input, got)
}
return
}
if !got.Equal(tc.want) {
t.Errorf("parseTime(%q) = %v, want %v", tc.input, got, tc.want)
}
})
}
}

func TestApiUser_FullName(t *testing.T) {
cases := []struct {
name string
first string
last string
want string
}{
{name: "both empty", first: "", last: "", want: ""},
{name: "first only", first: "Ada", last: "", want: "Ada"},
{name: "last only", first: "", last: "Lovelace", want: "Lovelace"},
{name: "both present", first: "Ada", last: "Lovelace", want: "Ada Lovelace"},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
u := apiUser{First: tc.first, Last: tc.last}
got := u.fullName()
if got != tc.want {
t.Errorf("apiUser{%q,%q}.fullName() = %q, want %q", tc.first, tc.last, got, tc.want)
}
})
}
}
61 changes: 61 additions & 0 deletions api/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package api

import "testing"

func TestSanitizeFilename(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{
name: "empty",
input: "",
want: "",
},
{
name: "plain alphanumeric",
input: "MyDesign",
want: "MyDesign",
},
{
name: "space and dot are allowed",
input: "My Design v2.0",
want: "My Design v2.0",
},
{
name: "path traversal — slashes replaced, dots kept",
input: "../../etc/passwd",
want: ".._.._etc_passwd",
},
{
name: "non-ASCII letters replaced",
input: "Caractères Spéciaux",
want: "Caract_res Sp_ciaux",
},
{
name: "all slashes become underscores (TrimSpace does not strip _)",
input: "////",
want: "____",
},
{
name: "leading and trailing whitespace trimmed",
input: " spaces ",
want: "spaces",
},
{
name: "null byte replaced",
input: "with\x00null",
want: "with_null",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := sanitizeFilename(tc.input)
if got != tc.want {
t.Errorf("sanitizeFilename(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}
42 changes: 42 additions & 0 deletions api/queries_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package api

import "testing"

func TestNavItemFromTypename(t *testing.T) {
cases := []struct {
name string
typename string
wantKind string
wantIsContainer bool
}{
{name: "DesignItem", typename: "DesignItem", wantKind: "design", wantIsContainer: false},
{name: "ConfiguredDesignItem", typename: "ConfiguredDesignItem", wantKind: "configured", wantIsContainer: false},
{name: "DrawingItem", typename: "DrawingItem", wantKind: "drawing", wantIsContainer: false},
{name: "Folder", typename: "Folder", wantKind: "folder", wantIsContainer: true},
{name: "unknown typename", typename: "MysteryType", wantKind: "unknown", wantIsContainer: false},
{name: "empty typename", typename: "", wantKind: "unknown", wantIsContainer: false},
}

const (
id = "urn:test:item:123"
name = "Test Name"
)

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := navItemFromTypename(id, name, tc.typename)
if got.ID != id {
t.Errorf("ID = %q, want %q", got.ID, id)
}
if got.Name != name {
t.Errorf("Name = %q, want %q", got.Name, name)
}
if got.Kind != tc.wantKind {
t.Errorf("Kind = %q, want %q", got.Kind, tc.wantKind)
}
if got.IsContainer != tc.wantIsContainer {
t.Errorf("IsContainer = %v, want %v", got.IsContainer, tc.wantIsContainer)
}
})
}
}
73 changes: 73 additions & 0 deletions auth/oauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package auth

import (
"net/url"
"testing"
)

func TestNewVerifier_Length(t *testing.T) {
v, err := newVerifier()
if err != nil {
t.Fatalf("newVerifier() returned error: %v", err)
}
if got, want := len(v), 86; got != want {
t.Errorf("newVerifier() length = %d, want %d (verifier=%q)", got, want, v)
}
}

func TestNewVerifier_Uniqueness(t *testing.T) {
const n = 100
seen := make(map[string]bool, n)
for i := 0; i < n; i++ {
v, err := newVerifier()
if err != nil {
t.Fatalf("newVerifier() iteration %d returned error: %v", i, err)
}
if seen[v] {
t.Fatalf("newVerifier() returned duplicate value at iteration %d: %q", i, v)
}
seen[v] = true
}
if len(seen) != n {
t.Errorf("expected %d unique verifiers, got %d", n, len(seen))
}
}

func TestVerifierToChallenge_RFCExample(t *testing.T) {
// RFC 7636 Appendix B test vector.
const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
const expected = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
if got := verifierToChallenge(verifier); got != expected {
t.Errorf("verifierToChallenge(%q) = %q, want %q", verifier, got, expected)
}
}

func TestBuildAuthURL_Shape(t *testing.T) {
raw := buildAuthURL("my-client-id", "my-challenge")
u, err := url.Parse(raw)
if err != nil {
t.Fatalf("url.Parse(%q) returned error: %v", raw, err)
}

if got, want := u.Host, "developer.api.autodesk.com"; got != want {
t.Errorf("host = %q, want %q", got, want)
}
if got, want := u.Path, "/authentication/v2/authorize"; got != want {
t.Errorf("path = %q, want %q", got, want)
}

q := u.Query()
wantParams := map[string]string{
"client_id": "my-client-id",
"response_type": "code",
"redirect_uri": CallbackURL,
"scope": "data:read user-profile:read",
"code_challenge": "my-challenge",
"code_challenge_method": "S256",
}
for k, want := range wantParams {
if got := q.Get(k); got != want {
t.Errorf("query[%q] = %q, want %q", k, got, want)
}
}
}
Loading
Loading