Compare commits

...

7 Commits

10 changed files with 65 additions and 58 deletions

View File

@@ -56,16 +56,16 @@ func newProgressIndicatorWithClock(total int, description string, c clock) *Prog
func (p *ProgressIndicator) Update(current int) {
p.mu.Lock()
defer p.mu.Unlock()
p.current = current
now := p.clock.Now()
if now.Sub(p.lastUpdate) < 100*time.Millisecond {
p.mu.Unlock()
return
}
p.lastUpdate = now
p.mu.Unlock()
p.display()
}

View File

@@ -44,15 +44,14 @@ func captureOutput(fn func()) string {
r, w, _ := os.Pipe()
os.Stdout = w
defer func() {
_ = w.Close()
os.Stdout = old
}()
fn()
_ = w.Close()
os.Stdout = old
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
_ = r.Close()
return buf.String()
}

View File

@@ -21,6 +21,8 @@ var (
seedRandOnce sync.Once
)
const testPasswordHash = "test_password_hash"
func initSeedRand() {
seedRandOnce.Do(func() {
seed := time.Now().UnixNano()
@@ -61,11 +63,11 @@ func TestSeedCommand(t *testing.T) {
seedUserCount := 0
var seedUser *database.User
regularUserCount := 0
for i := range users {
if users[i].Username == "seed_admin" {
for idx := range users {
if users[idx].Username == seedUsername {
seedUserCount++
seedUser = &users[i]
} else if strings.HasPrefix(users[i].Username, "user_") {
seedUser = &users[idx]
} else if strings.HasPrefix(users[idx].Username, "user_") {
regularUserCount++
}
}
@@ -78,12 +80,12 @@ func TestSeedCommand(t *testing.T) {
t.Fatal("Expected seed user to be created")
}
if seedUser.Username != "seed_admin" {
t.Errorf("Expected username to be 'seed_admin', got '%s'", seedUser.Username)
if seedUser.Username != seedUsername {
t.Errorf("Expected username to be %q, got '%s'", seedUsername, seedUser.Username)
}
if seedUser.Email != "seed_admin@goyco.local" {
t.Errorf("Expected email to be 'seed_admin@goyco.local', got '%s'", seedUser.Email)
if seedUser.Email != seedEmail {
t.Errorf("Expected email to be %q, got '%s'", seedEmail, seedUser.Email)
}
if !seedUser.EmailVerified {
@@ -103,20 +105,20 @@ func TestSeedCommand(t *testing.T) {
t.Errorf("Expected 5 posts, got %d", len(posts))
}
for i, post := range posts {
for idx, post := range posts {
if post.Title == "" {
t.Errorf("Post %d has empty title", i)
t.Errorf("Post %d has empty title", idx)
}
if post.URL == "" {
t.Errorf("Post %d has empty URL", i)
t.Errorf("Post %d has empty URL", idx)
}
if post.AuthorID == nil || *post.AuthorID != seedUser.ID {
t.Errorf("Post %d has wrong author ID: expected %d, got %v", i, seedUser.ID, post.AuthorID)
t.Errorf("Post %d has wrong author ID: expected %d, got %v", idx, seedUser.ID, post.AuthorID)
}
expectedScore := post.UpVotes - post.DownVotes
if post.Score != expectedScore {
t.Errorf("Post %d has incorrect score: expected %d, got %d", i, expectedScore, post.Score)
t.Errorf("Post %d has incorrect score: expected %d, got %d", idx, expectedScore, post.Score)
}
}
@@ -148,11 +150,12 @@ func TestSeedCommand(t *testing.T) {
}
func TestGenerateRandomPath(t *testing.T) {
const articlePathPrefix = "/article/"
initSeedRand()
pathLength := seedRandSource.Intn(20)
path := "/article/"
path := articlePathPrefix
for i := 0; i < pathLength+5; i++ {
for idx := 0; idx < pathLength+5; idx++ {
randomChar := seedRandSource.Intn(26)
path += string(rune('a' + randomChar))
}
@@ -167,13 +170,14 @@ func TestGenerateRandomPath(t *testing.T) {
initSeedRand()
secondPathLength := seedRandSource.Intn(20)
secondPath := "/article/"
for i := 0; i < secondPathLength+5; i++ {
var secondPath strings.Builder
secondPath.WriteString(articlePathPrefix)
for idx := 0; idx < secondPathLength+5; idx++ {
randomChar := seedRandSource.Intn(26)
secondPath += string(rune('a' + randomChar))
secondPath.WriteString(string(rune('a' + randomChar)))
}
if path == secondPath {
if path == secondPath.String() {
t.Error("Generated paths should be different")
}
}
@@ -333,7 +337,7 @@ func TestSeedCommandIdempotency(t *testing.T) {
seedUserCount := 0
for _, user := range users {
if user.Username == "seed_admin" {
if user.Username == seedUsername {
seedUserCount++
}
}
@@ -369,10 +373,10 @@ func TestSeedCommandIdempotency(t *testing.T) {
})
t.Run("database remains consistent after multiple runs", func(t *testing.T) {
for i := range 2 {
for idx := range 2 {
err := seedDatabase(userRepo, postRepo, voteRepo, []string{"--users", "0", "--posts", "1"})
if err != nil {
t.Fatalf("Seed run %d failed: %v", i+1, err)
t.Fatalf("Seed run %d failed: %v", idx+1, err)
}
}
@@ -417,9 +421,9 @@ func TestSeedCommandIdempotency(t *testing.T) {
}
func findSeedUser(users []database.User) *database.User {
for i := range users {
if users[i].Username == "seed_admin" {
return &users[i]
for idx := range users {
if users[idx].Username == seedUsername {
return &users[idx]
}
}
return nil
@@ -519,14 +523,14 @@ func TestEnsureSeedUser(t *testing.T) {
}
userRepo := repositories.NewUserRepository(db)
passwordHash := "test_password_hash"
passwordHash := testPasswordHash
firstUser, err := ensureSeedUser(userRepo, passwordHash)
if err != nil {
t.Fatalf("Failed to create seed user: %v", err)
}
if firstUser.Username != "seed_admin" || firstUser.Email != "seed_admin@goyco.local" || firstUser.Password != passwordHash || !firstUser.EmailVerified {
if firstUser.Username != seedUsername || firstUser.Email != seedEmail || firstUser.Password != passwordHash || !firstUser.EmailVerified {
t.Errorf("Invalid seed user: username=%s, email=%s, password matches=%v, emailVerified=%v",
firstUser.Username, firstUser.Email, firstUser.Password == passwordHash, firstUser.EmailVerified)
}
@@ -540,9 +544,9 @@ func TestEnsureSeedUser(t *testing.T) {
t.Errorf("Expected same user to be reused (ID %d), got different user (ID %d)", firstUser.ID, secondUser.ID)
}
for i := 0; i < 3; i++ {
for idx := range 3 {
if _, err := ensureSeedUser(userRepo, passwordHash); err != nil {
t.Fatalf("Call %d failed: %v", i+1, err)
t.Fatalf("Call %d failed: %v", idx+1, err)
}
}
@@ -553,7 +557,7 @@ func TestEnsureSeedUser(t *testing.T) {
seedUserCount := 0
for _, user := range users {
if user.Username == "seed_admin" {
if user.Username == seedUsername {
seedUserCount++
}
}
@@ -565,7 +569,7 @@ func TestEnsureSeedUser(t *testing.T) {
func TestEnsureSeedUser_HandlesDatabaseErrors(t *testing.T) {
userRepo := testutils.NewMockUserRepository()
passwordHash := "test_password_hash"
passwordHash := testPasswordHash
dbError := fmt.Errorf("database connection failed")

View File

@@ -1,5 +1,5 @@
// @title Goyco API
// @version 0.1.0
// @version 0.1.1
// @description Goyco is a Y Combinator-style news aggregation platform API.
// @contact.name Goyco Team
// @contact.email sandro@cazzaniga.fr
@@ -55,7 +55,7 @@ func run(args []string) error {
docs.SwaggerInfo.Title = fmt.Sprintf("%s API", cfg.App.Title)
docs.SwaggerInfo.Description = "Y Combinator-style news board API."
docs.SwaggerInfo.Version = version.Version
docs.SwaggerInfo.Version = version.GetVersion()
docs.SwaggerInfo.BasePath = "/api"
docs.SwaggerInfo.Host = fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
docs.SwaggerInfo.Schemes = []string{"http"}

View File

@@ -2129,7 +2129,7 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1.0",
Version: "0.1.1",
Host: "localhost:8080",
BasePath: "/api",
Schemes: []string{"http"},

View File

@@ -14,7 +14,7 @@
"name": "GPLv3",
"url": "https://www.gnu.org/licenses/gpl-3.0.html"
},
"version": "0.1.0"
"version": "0.1.1"
},
"host": "localhost:8080",
"basePath": "/api",

View File

@@ -221,7 +221,7 @@ info:
name: GPLv3
url: https://www.gnu.org/licenses/gpl-3.0.html
title: Goyco API
version: 0.1.0
version: 0.1.1
paths:
/api:
get:

View File

@@ -75,7 +75,7 @@ func (h *APIHandler) GetAPIInfo(w http.ResponseWriter, r *http.Request) {
apiInfo := map[string]any{
"name": fmt.Sprintf("%s API", h.config.App.Title),
"version": version.Version,
"version": version.GetVersion(),
"description": "Y Combinator-style news board API",
"endpoints": map[string]any{
"authentication": map[string]any{
@@ -145,7 +145,7 @@ func (h *APIHandler) GetHealth(w http.ResponseWriter, r *http.Request) {
if h.healthChecker != nil {
health := h.healthChecker.CheckHealth()
health["version"] = version.Version
health["version"] = version.GetVersion()
SendSuccessResponse(w, "Health check successful", health)
return
}
@@ -155,7 +155,7 @@ func (h *APIHandler) GetHealth(w http.ResponseWriter, r *http.Request) {
health := map[string]any{
"status": "healthy",
"timestamp": currentTimestamp,
"version": version.Version,
"version": version.GetVersion(),
"services": map[string]any{
"database": "connected",
"api": "running",
@@ -230,7 +230,7 @@ func (h *APIHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
},
"system": map[string]any{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"version": version.Version,
"version": version.GetVersion(),
},
}

View File

@@ -1,3 +1,7 @@
package version
const Version = "0.1.0"
const version = "0.1.1"
func GetVersion() string {
return version
}

View File

@@ -8,39 +8,39 @@ import (
func TestVersionSemver(t *testing.T) {
semverRegex := regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
if !semverRegex.MatchString(Version) {
t.Errorf("Version %q does not follow semantic versioning format (MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD])", Version)
if !semverRegex.MatchString(GetVersion()) {
t.Errorf("Version %q does not follow semantic versioning format (MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD])", GetVersion())
}
}
func TestVersionSemverFlexible(t *testing.T) {
flexibleSemverRegex := regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
if !flexibleSemverRegex.MatchString(Version) {
t.Errorf("Version %q does not follow semantic versioning format (MAJOR.MINOR[.PATCH][-PRERELEASE][+BUILD])", Version)
if !flexibleSemverRegex.MatchString(GetVersion()) {
t.Errorf("Version %q does not follow semantic versioning format (MAJOR.MINOR[.PATCH][-PRERELEASE][+BUILD])", GetVersion())
}
}
func TestVersionNotEmpty(t *testing.T) {
if Version == "" {
if GetVersion() == "" {
t.Error("Version should not be empty")
}
}
func TestVersionFormat(t *testing.T) {
if !regexp.MustCompile(`\d+\.\d+`).MatchString(Version) {
t.Errorf("Version %q should contain at least MAJOR.MINOR format", Version)
if !regexp.MustCompile(`\d+\.\d+`).MatchString(GetVersion()) {
t.Errorf("Version %q should contain at least MAJOR.MINOR format", GetVersion())
}
}
func TestVersionStartsWithNumber(t *testing.T) {
if !regexp.MustCompile(`^\d+`).MatchString(Version) {
t.Errorf("Version %q should start with a number", Version)
if !regexp.MustCompile(`^\d+`).MatchString(GetVersion()) {
t.Errorf("Version %q should start with a number", GetVersion())
}
}
func TestVersionNoLeadingZeros(t *testing.T) {
parts := regexp.MustCompile(`^(\d+)\.(\d+)`).FindStringSubmatch(Version)
parts := regexp.MustCompile(`^(\d+)\.(\d+)`).FindStringSubmatch(GetVersion())
if len(parts) >= 3 {
major := parts[1]
minor := parts[2]