// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package owner import ( "context" "errors" "os" "path/filepath" "strings" "sync" "testing" ) func newTestStore(t *testing.T) *FileOwnerStore { t.Helper() s, err := NewFileOwnerStore(t.TempDir()) if err != nil { t.Fatal(err) } return s } func TestHashAndVerifyPassword(t *testing.T) { hash, err := HashPassword("correct horse battery") if err != nil { t.Fatal(err) } if !strings.HasPrefix(hash, "$argon2id$v=19$m=65536,t=3,p=2$") { t.Fatalf("hash missing argon2id PHC prefix: %s", hash) } if !VerifyPassword(hash, "correct horse battery") { t.Fatal("correct password rejected") } if VerifyPassword(hash, "wrong password!") { t.Fatal("wrong password accepted") } if VerifyPassword("garbage", "x") { t.Fatal("garbage hash accepted") } } func TestValidateCredentials(t *testing.T) { good := [][2]string{{"admin", "longenough123"}, {"a.b-c_d1", "0123456789"}} for _, c := range good { if err := ValidateCredentials(c[0], c[1]); err != nil { t.Errorf("%q/%q rejected: %v", c[0], c[1], err) } } bad := [][2]string{ {"Admin", "0123456789"}, // uppercase {"-lead", "0123456789"}, // bad first char {"ab", "0123456789"}, // too short {"okname", "short"}, // weak password } for _, c := range bad { if err := ValidateCredentials(c[0], c[1]); err == nil { t.Errorf("%q/%q accepted", c[0], c[1]) } } } func TestStoreRoundtripAndMultiUser(t *testing.T) { ctx := context.Background() s := newTestStore(t) if n, _ := s.Count(ctx); n != 0 { t.Fatalf("fresh store Count = %d", n) } first, err := s.Create(ctx, "admin", "first-owner-pass", true) if err != nil { t.Fatal(err) } if !first.IsAdmin || first.Role() != "admin" { t.Fatalf("first owner should be admin, got %+v", first) } second, err := s.Create(ctx, "member", "second-owner-pass", false) if err != nil { t.Fatal(err) } if second.Role() != "member" { t.Fatalf("second owner role = %q", second.Role()) } if n, _ := s.Count(ctx); n != 2 { t.Fatalf("Count = %d, want 2", n) } if got, err := s.Verify(ctx, "admin", "first-owner-pass"); err != nil || got.Username != "admin" { t.Fatalf("Verify admin: %v %+v", err, got) } if _, err := s.Verify(ctx, "admin", "wrong"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("wrong password: %v", err) } if _, err := s.Create(ctx, "admin", "whatever-123", false); !errors.Is(err, os.ErrExist) { t.Fatalf("duplicate create: %v", err) } changed, err := s.ChangePassword(ctx, "admin", "first-owner-pass", "rotated-password") if err != nil { t.Fatal(err) } if changed.UpdatedAt.Equal(changed.CreatedAt) { t.Fatal("updated_at not bumped") } if _, err := s.Verify(ctx, "admin", "first-owner-pass"); err == nil { t.Fatal("old password still valid") } if _, err := s.Verify(ctx, "admin", "rotated-password"); err != nil { t.Fatalf("new password rejected: %v", err) } } func TestConcurrentCreateIsIdempotent(t *testing.T) { ctx := context.Background() s := newTestStore(t) var wg sync.WaitGroup errs := make(chan error, 16) for i := 0; i < 16; i++ { wg.Add(1) go func() { defer wg.Done() if _, err := s.Create(ctx, "wizard", "setup-password", true); err != nil && !errors.Is(err, os.ErrExist) { errs <- err } }() } wg.Wait() close(errs) for err := range errs { t.Fatalf("unexpected error: %v", err) } if n, _ := s.Count(ctx); n != 1 { t.Fatalf("Count after concurrent setup = %d, want 1", n) } } func TestOwnerFilePermissions(t *testing.T) { ctx := context.Background() dir := t.TempDir() s, err := NewFileOwnerStore(dir) if err != nil { t.Fatal(err) } if _, err := s.Create(ctx, "admin", "perm-check-pass", true); err != nil { t.Fatal(err) } info, err := os.Stat(filepath.Join(dir, "owners.json")) if err != nil { t.Fatal(err) } if perm := info.Mode().Perm(); perm != 0o600 { t.Fatalf("owners.json mode = %o, want 600", perm) } }