마일스톤 작업 생성 동기화를 진행하기 위해 Plane project별 active sync 설정을 Core DB에 저장하고 조회할 수 있는 persistence 경계와 검증 산출물을 함께 정리한다.
288 lines
8.7 KiB
Go
288 lines
8.7 KiB
Go
package projectsync
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nomadcode/nomadcode-core/internal/db"
|
|
"github.com/nomadcode/nomadcode-core/internal/workitem"
|
|
)
|
|
|
|
func TestConfigNormalizeRequiresProjectSyncFields(t *testing.T) {
|
|
input := Config{
|
|
Target: ProviderProjectTarget{
|
|
Provider: workitem.ProviderID(" plane "),
|
|
Tenant: " general ",
|
|
Project: " project-1 ",
|
|
},
|
|
GitRemoteURL: " git@example.com:nomadcode.git ",
|
|
SourceBranch: " develop ",
|
|
WorkspaceID: " main ",
|
|
WorkspaceBasePath: " /home/user/workspace/ ",
|
|
RepoDirName: " nomadcode ",
|
|
}
|
|
|
|
got, err := input.Normalize()
|
|
if err != nil {
|
|
t.Fatalf("Normalize returned error: %v", err)
|
|
}
|
|
|
|
if got.Target.Provider != "plane" {
|
|
t.Errorf("provider: got %q", got.Target.Provider)
|
|
}
|
|
if got.Target.Tenant != "general" {
|
|
t.Errorf("tenant: got %q", got.Target.Tenant)
|
|
}
|
|
if got.Target.Project != "project-1" {
|
|
t.Errorf("project: got %q", got.Target.Project)
|
|
}
|
|
if got.GitRemoteURL != "git@example.com:nomadcode.git" {
|
|
t.Errorf("git remote: got %q", got.GitRemoteURL)
|
|
}
|
|
if got.SourceBranch != "develop" {
|
|
t.Errorf("source branch: got %q", got.SourceBranch)
|
|
}
|
|
if got.WorkspaceID != "main" {
|
|
t.Errorf("workspace id: got %q", got.WorkspaceID)
|
|
}
|
|
if got.WorkspaceBasePath != "/home/user/workspace" {
|
|
t.Errorf("workspace base path: got %q", got.WorkspaceBasePath)
|
|
}
|
|
if got.RepoDirName != "nomadcode" {
|
|
t.Errorf("repo dir name: got %q", got.RepoDirName)
|
|
}
|
|
}
|
|
|
|
func TestConfigNormalizeRejectsMissingRequiredFields(t *testing.T) {
|
|
valid := Config{
|
|
Target: ProviderProjectTarget{
|
|
Provider: "plane",
|
|
Tenant: "general",
|
|
Project: "project-1",
|
|
},
|
|
GitRemoteURL: "git@example.com:nomadcode.git",
|
|
SourceBranch: "develop",
|
|
WorkspaceID: "main",
|
|
WorkspaceBasePath: "/home/user/workspace",
|
|
RepoDirName: "nomadcode",
|
|
}
|
|
|
|
tests := map[string]Config{
|
|
"provider": func() Config { c := valid; c.Target.Provider = ""; return c }(),
|
|
"tenant": func() Config { c := valid; c.Target.Tenant = ""; return c }(),
|
|
"project": func() Config { c := valid; c.Target.Project = ""; return c }(),
|
|
"git remote": func() Config { c := valid; c.GitRemoteURL = ""; return c }(),
|
|
"source branch": func() Config { c := valid; c.SourceBranch = ""; return c }(),
|
|
"workspace id": func() Config { c := valid; c.WorkspaceID = ""; return c }(),
|
|
"workspace base path": func() Config { c := valid; c.WorkspaceBasePath = ""; return c }(),
|
|
"repo dir name": func() Config { c := valid; c.RepoDirName = ""; return c }(),
|
|
}
|
|
|
|
for name, input := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := input.Normalize()
|
|
if !errors.Is(err, ErrInvalidConfig) {
|
|
t.Fatalf("Normalize error: got %v, want ErrInvalidConfig", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkspacePathsUseRepoRootAndZeroPaddedSlot(t *testing.T) {
|
|
root, err := ProjectWorkspaceRoot("/home/user/workspace", "nomadcode")
|
|
if err != nil {
|
|
t.Fatalf("ProjectWorkspaceRoot returned error: %v", err)
|
|
}
|
|
if root != "/home/user/workspace/nomadcode" {
|
|
t.Fatalf("root: got %q", root)
|
|
}
|
|
|
|
slotPath, err := SlotWorkspacePath("/home/user/workspace", "nomadcode", DefaultSlotIndex)
|
|
if err != nil {
|
|
t.Fatalf("SlotWorkspacePath returned error: %v", err)
|
|
}
|
|
if slotPath != "/home/user/workspace/nomadcode/000" {
|
|
t.Fatalf("slot path: got %q", slotPath)
|
|
}
|
|
}
|
|
|
|
func TestSlotIndexFormattingAndBounds(t *testing.T) {
|
|
slot, err := NewSlotIndex(2)
|
|
if err != nil {
|
|
t.Fatalf("NewSlotIndex returned error: %v", err)
|
|
}
|
|
if slot.String() != "002" {
|
|
t.Fatalf("slot string: got %q", slot.String())
|
|
}
|
|
|
|
for _, index := range []int{-1, 1000} {
|
|
t.Run("invalid", func(t *testing.T) {
|
|
if _, err := NewSlotIndex(index); !errors.Is(err, ErrInvalidConfig) {
|
|
t.Fatalf("NewSlotIndex(%d) error: got %v, want ErrInvalidConfig", index, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSlotStateValidationAndAllocation(t *testing.T) {
|
|
if !SlotStateAvailable.Valid() || !SlotStateInUse.Valid() || !SlotStateDirty.Valid() || !SlotStateError.Valid() {
|
|
t.Fatal("expected known slot states to be valid")
|
|
}
|
|
if !SlotStateAvailable.Allocatable() {
|
|
t.Fatal("available slot should be allocatable")
|
|
}
|
|
if SlotStateInUse.Allocatable() || SlotStateDirty.Allocatable() || SlotStateError.Allocatable() {
|
|
t.Fatal("only available slots should be allocatable")
|
|
}
|
|
if SlotState("unknown").Valid() {
|
|
t.Fatal("unknown slot state should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestRepoDirNameRejectsNestedOrAbsolutePaths(t *testing.T) {
|
|
for _, repoDirName := range []string{"/nomadcode", "../nomadcode", "team/nomadcode", `team\nomadcode`} {
|
|
t.Run(repoDirName, func(t *testing.T) {
|
|
_, err := ProjectWorkspaceRoot("/home/user/workspace", repoDirName)
|
|
if !errors.Is(err, ErrInvalidConfig) {
|
|
t.Fatalf("ProjectWorkspaceRoot error: got %v, want ErrInvalidConfig", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigToCreateProjectSyncParams verifies the DB params conversion.
|
|
func TestConfigToCreateProjectSyncParams(t *testing.T) {
|
|
config := Config{
|
|
Target: ProviderProjectTarget{
|
|
Provider: "plane",
|
|
Tenant: "general",
|
|
Project: "project-x",
|
|
},
|
|
GitRemoteURL: "git@gh.com:org/repo.git",
|
|
SourceBranch: "main",
|
|
WorkspaceID: "ws-abc",
|
|
WorkspaceBasePath: "/data/ws",
|
|
RepoDirName: "repo",
|
|
}
|
|
|
|
// Normalize first (required by contract)
|
|
normal, err := config.Normalize()
|
|
if err != nil {
|
|
t.Fatalf("Normalize failed: %v", err)
|
|
}
|
|
|
|
params := normal.ToCreateProjectSyncParams()
|
|
if params.Provider != "plane" {
|
|
t.Errorf("Provider: got %q", params.Provider)
|
|
}
|
|
if params.Tenant != "general" {
|
|
t.Errorf("Tenant: got %q", params.Tenant)
|
|
}
|
|
if params.Project != "project-x" {
|
|
t.Errorf("Project: got %q", params.Project)
|
|
}
|
|
if params.GitRemoteUrl != "git@gh.com:org/repo.git" {
|
|
t.Errorf("GitRemoteUrl: got %q", params.GitRemoteUrl)
|
|
}
|
|
if params.SourceBranch != "main" {
|
|
t.Errorf("SourceBranch: got %q", params.SourceBranch)
|
|
}
|
|
if params.WorkspaceID != "ws-abc" {
|
|
t.Errorf("WorkspaceID: got %q", params.WorkspaceID)
|
|
}
|
|
if params.WorkspaceBasePath != "/data/ws" {
|
|
t.Errorf("WorkspaceBasePath: got %q", params.WorkspaceBasePath)
|
|
}
|
|
if params.RepoDirName != "repo" {
|
|
t.Errorf("RepoDirName: got %q", params.RepoDirName)
|
|
}
|
|
}
|
|
|
|
// TestConfigFromDBRecord verifies the DB record → Config conversion.
|
|
func TestConfigFromDBRecord(t *testing.T) {
|
|
now := time.Now()
|
|
record := db.ProjectSyncSetting{
|
|
ID: 7,
|
|
Provider: "plane",
|
|
Tenant: "org2",
|
|
Project: "proj-y",
|
|
GitRemoteUrl: "git@bitbucket.com:team/app.git",
|
|
SourceBranch: "develop",
|
|
WorkspaceID: "ws-def",
|
|
WorkspaceBasePath: "/srv/ws",
|
|
RepoDirName: "app",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
cfg := ConfigFromDBRecord(record)
|
|
if cfg.Target.Provider != workitem.ProviderID("plane") {
|
|
t.Errorf("Target.Provider: got %q", cfg.Target.Provider)
|
|
}
|
|
if cfg.Target.Tenant != "org2" {
|
|
t.Errorf("Target.Tenant: got %q", cfg.Target.Tenant)
|
|
}
|
|
if cfg.Target.Project != "proj-y" {
|
|
t.Errorf("Target.Project: got %q", cfg.Target.Project)
|
|
}
|
|
if cfg.GitRemoteURL != "git@bitbucket.com:team/app.git" {
|
|
t.Errorf("GitRemoteURL: got %q", cfg.GitRemoteURL)
|
|
}
|
|
if cfg.SourceBranch != "develop" {
|
|
t.Errorf("SourceBranch: got %q", cfg.SourceBranch)
|
|
}
|
|
if cfg.WorkspaceID != "ws-def" {
|
|
t.Errorf("WorkspaceID: got %q", cfg.WorkspaceID)
|
|
}
|
|
if cfg.WorkspaceBasePath != "/srv/ws" {
|
|
t.Errorf("WorkspaceBasePath: got %q", cfg.WorkspaceBasePath)
|
|
}
|
|
if cfg.RepoDirName != "app" {
|
|
t.Errorf("RepoDirName: got %q", cfg.RepoDirName)
|
|
}
|
|
}
|
|
|
|
// TestConfigRoundTrip verifies Config → DB params → Config preserves fields.
|
|
func TestConfigRoundTrip(t *testing.T) {
|
|
original := Config{
|
|
Target: ProviderProjectTarget{
|
|
Provider: "jira",
|
|
Tenant: "acme",
|
|
Project: "board-42",
|
|
},
|
|
GitRemoteURL: "git@github.com:acme/core.git",
|
|
SourceBranch: "release/v1",
|
|
WorkspaceID: "ws-rt",
|
|
WorkspaceBasePath: "/mnt/ws",
|
|
RepoDirName: "core",
|
|
}
|
|
|
|
normal, err := original.Normalize()
|
|
if err != nil {
|
|
t.Fatalf("Normalize: %v", err)
|
|
}
|
|
|
|
params := normal.ToCreateProjectSyncParams()
|
|
record := db.ProjectSyncSetting{
|
|
ID: 99,
|
|
Provider: params.Provider,
|
|
Tenant: params.Tenant,
|
|
Project: params.Project,
|
|
GitRemoteUrl: params.GitRemoteUrl,
|
|
SourceBranch: params.SourceBranch,
|
|
WorkspaceID: params.WorkspaceID,
|
|
WorkspaceBasePath: params.WorkspaceBasePath,
|
|
RepoDirName: params.RepoDirName,
|
|
Active: true,
|
|
}
|
|
|
|
recovered := ConfigFromDBRecord(record)
|
|
if recovered.Target.Provider != normal.Target.Provider {
|
|
t.Errorf("Provider round-trip: got %q", recovered.Target.Provider)
|
|
}
|
|
if recovered.GitRemoteURL != normal.GitRemoteURL {
|
|
t.Errorf("GitRemoteURL round-trip: got %q", recovered.GitRemoteURL)
|
|
}
|
|
}
|