Skip to content

Commit dc8dcc4

Browse files
nslussAlephNotation
authored andcommitted
test: add unit tests for run-commit --ref feature
Two new files, matching the existing repo test style (tags_test.go pattern for handler tests via httptest.NewServer, routing_test.go table-driven pattern for cmd-level pure-function tests): internal/handlers/run_commit_test.go: - TestHandleRunCommit_CommitIDPath — verifies default (IsRef=false) sends commit_id, no ref key in body - TestHandleRunCommit_RefPath — verifies IsRef=true sends ref, no commit_id key - TestHandleRunCommit_ServerError — verifies non-2xx surfaces as a caller-visible error (no silent swallowing) cmd/run_commit_test.go: - TestLooksLikeRepoRef (table-driven, 16 cases) — positive: canonical repo:tag shapes with various legal chars. Negative: empty/no-colon, malformed colon placement, chars outside the legal name set (space, slash, @, #, non-ASCII). - TestLooksLikeRepoRef_MultipleColons — documents the current (intended) behavior for a:b:c as NOT matching, since the second colon isn't a legal name char.
1 parent 759b21d commit dc8dcc4

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

cmd/run_commit_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
// TestLooksLikeRepoRef verifies the heuristic used to suggest --ref when
6+
// the user passes a repo:tag-shaped argument. False positives are cheap
7+
// (we just show an extra suggestion); false negatives mean the user hits
8+
// a cryptic 422 from the API, so the positive cases get careful coverage.
9+
func TestLooksLikeRepoRef(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
want bool
14+
}{
15+
// Positive: canonical repo:tag shapes
16+
{"simple", "my-app:latest", true},
17+
{"minimal", "a:b", true},
18+
{"semver-tag", "my_app:v1.0.0", true},
19+
{"dotted-parts", "x.y:1.2", true},
20+
{"numeric-parts", "123:456", true},
21+
{"mixed-legal-chars", "pi-agent:2026-04-19-v4", true},
22+
23+
// Negative: no colon
24+
{"empty", "", false},
25+
{"no-colon", "abc-123", false},
26+
{"uuid", "aa84beb8-a080-4be5-bb7f-86374a68b380", false},
27+
28+
// Negative: malformed colon placement
29+
{"colon-at-start", ":latest", false},
30+
{"colon-at-end", "my-app:", false},
31+
{"lone-colon", ":", false},
32+
33+
// Negative: contains characters not valid in repo/tag names
34+
{"space", "my app:latest", false},
35+
{"slash", "owner/my-app:latest", false},
36+
{"at-sign", "my-app@v1:latest", false},
37+
{"hash", "my-app#1:latest", false},
38+
{"non-ascii", "my-app:päivä", false},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
got := looksLikeRepoRef(tt.input)
44+
if got != tt.want {
45+
t.Errorf("looksLikeRepoRef(%q) = %v, want %v", tt.input, got, tt.want)
46+
}
47+
})
48+
}
49+
}
50+
51+
// TestLooksLikeRepoRef_MultipleColons documents the current behavior for
52+
// strings with more than one colon. The first colon splits repo from tag,
53+
// and subsequent characters (including another colon) must be legal name
54+
// chars. A bare `a:b:c` has a colon in the tag part, which fails the
55+
// legal-char check and returns false.
56+
func TestLooksLikeRepoRef_MultipleColons(t *testing.T) {
57+
if looksLikeRepoRef("a:b:c") {
58+
t.Error("expected a:b:c to NOT look like a repo:tag (colon in tag part is not a legal name char)")
59+
}
60+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package handlers_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/hdresearch/vers-cli/internal/handlers"
12+
)
13+
14+
// TestHandleRunCommit_CommitIDPath verifies that when IsRef is false, the
15+
// request body places the argument under "commit_id" (no "ref" key).
16+
// This is the default behavior — any existing callers must not regress.
17+
func TestHandleRunCommit_CommitIDPath(t *testing.T) {
18+
var receivedBody map[string]interface{}
19+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
if r.URL.Path != "/api/v1/vm/from_commit" {
21+
t.Errorf("unexpected path: %s", r.URL.Path)
22+
}
23+
if r.Method != http.MethodPost {
24+
t.Errorf("expected POST, got %s", r.Method)
25+
}
26+
body, _ := io.ReadAll(r.Body)
27+
json.Unmarshal(body, &receivedBody)
28+
29+
w.Header().Set("Content-Type", "application/json")
30+
w.WriteHeader(http.StatusOK)
31+
w.Write([]byte(`{"vm_id": "vm-uuid-1"}`))
32+
}))
33+
defer server.Close()
34+
35+
a := testApp(server.URL)
36+
res, err := handlers.HandleRunCommit(context.Background(), a, handlers.RunCommitReq{
37+
CommitKey: "abc-123",
38+
IsRef: false,
39+
})
40+
if err != nil {
41+
t.Fatalf("unexpected error: %v", err)
42+
}
43+
if res.RootVmID != "vm-uuid-1" {
44+
t.Errorf("expected VM ID vm-uuid-1, got %s", res.RootVmID)
45+
}
46+
if res.CommitKey != "abc-123" {
47+
t.Errorf("expected CommitKey abc-123, got %s", res.CommitKey)
48+
}
49+
50+
// The SDK flattens the vm_from_commit_request union into the top-level
51+
// request body (MarshalJSON on VmRestoreFromCommitParams inlines it).
52+
if receivedBody["commit_id"] != "abc-123" {
53+
t.Errorf("expected commit_id=abc-123, got %v", receivedBody["commit_id"])
54+
}
55+
if _, refPresent := receivedBody["ref"]; refPresent {
56+
t.Errorf("expected no 'ref' key when IsRef=false, got: %v", receivedBody)
57+
}
58+
}
59+
60+
// TestHandleRunCommit_RefPath verifies that when IsRef is true, the request
61+
// body uses "ref" instead of "commit_id". This is the new behavior added
62+
// by the --ref flag.
63+
func TestHandleRunCommit_RefPath(t *testing.T) {
64+
var receivedBody map[string]interface{}
65+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
if r.URL.Path != "/api/v1/vm/from_commit" {
67+
t.Errorf("unexpected path: %s", r.URL.Path)
68+
}
69+
if r.Method != http.MethodPost {
70+
t.Errorf("expected POST, got %s", r.Method)
71+
}
72+
body, _ := io.ReadAll(r.Body)
73+
json.Unmarshal(body, &receivedBody)
74+
75+
w.Header().Set("Content-Type", "application/json")
76+
w.WriteHeader(http.StatusOK)
77+
w.Write([]byte(`{"vm_id": "vm-uuid-2"}`))
78+
}))
79+
defer server.Close()
80+
81+
a := testApp(server.URL)
82+
res, err := handlers.HandleRunCommit(context.Background(), a, handlers.RunCommitReq{
83+
CommitKey: "my-app:latest",
84+
IsRef: true,
85+
})
86+
if err != nil {
87+
t.Fatalf("unexpected error: %v", err)
88+
}
89+
if res.RootVmID != "vm-uuid-2" {
90+
t.Errorf("expected VM ID vm-uuid-2, got %s", res.RootVmID)
91+
}
92+
if res.CommitKey != "my-app:latest" {
93+
t.Errorf("expected CommitKey my-app:latest, got %s", res.CommitKey)
94+
}
95+
96+
// The SDK flattens the vm_from_commit_request union into the top-level
97+
// request body (MarshalJSON on VmRestoreFromCommitParams inlines it).
98+
if receivedBody["ref"] != "my-app:latest" {
99+
t.Errorf("expected ref=my-app:latest, got %v", receivedBody["ref"])
100+
}
101+
if _, commitIDPresent := receivedBody["commit_id"]; commitIDPresent {
102+
t.Errorf("expected no 'commit_id' key when IsRef=true, got: %v", receivedBody)
103+
}
104+
}
105+
106+
// TestHandleRunCommit_ServerError verifies that non-2xx responses surface as
107+
// errors to the caller (no silent swallowing).
108+
func TestHandleRunCommit_ServerError(t *testing.T) {
109+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110+
w.Header().Set("Content-Type", "application/json")
111+
w.WriteHeader(http.StatusUnprocessableEntity)
112+
w.Write([]byte(`{"error": "invalid commit id"}`))
113+
}))
114+
defer server.Close()
115+
116+
a := testApp(server.URL)
117+
_, err := handlers.HandleRunCommit(context.Background(), a, handlers.RunCommitReq{
118+
CommitKey: "not-a-uuid",
119+
IsRef: false,
120+
})
121+
if err == nil {
122+
t.Fatal("expected error for 422 response, got nil")
123+
}
124+
}

0 commit comments

Comments
 (0)