mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Tests: Add unit tests for recent metadata, auth, and config changes
This commit is contained in:
parent
9fe532914d
commit
c7fa52cf13
5 changed files with 120 additions and 4 deletions
|
|
@ -165,6 +165,35 @@ describe("common/config", () => {
|
||||||
expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg");
|
expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("getIcon resolves a built-in appIcon name against the static uri", () => {
|
||||||
|
const config = new Config(new StorageShim(), { siteTitle: "Foo", baseUri: "/i/pro-1", appIcon: "bloom" });
|
||||||
|
expect(config.getIcon()).toBe("/i/pro-1/static/icons/bloom.svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getIcon resolves each selectable app-icon variant to its static svg", () => {
|
||||||
|
["crisp", "mint", "bold", "bloom", "flower", "ring", "shutter"].forEach((name) => {
|
||||||
|
const config = new Config(new StorageShim(), { siteTitle: "Foo", appIcon: name });
|
||||||
|
expect(config.getIcon()).toBe(`/static/icons/${name}.svg`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getIcon falls back to the logo for an unknown or empty app-icon name", () => {
|
||||||
|
expect(new Config(new StorageShim(), { siteTitle: "Foo", appIcon: "does-not-exist" }).getIcon()).toBe("/static/icons/logo.svg");
|
||||||
|
expect(new Config(new StorageShim(), { siteTitle: "Foo" }).getIcon()).toBe("/static/icons/logo.svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getIcon prefers a theme-provided icon over the appIcon setting", () => {
|
||||||
|
const config = new Config(new StorageShim(), {
|
||||||
|
siteTitle: "Foo",
|
||||||
|
baseUri: "/i/pro-1",
|
||||||
|
appIcon: "bloom",
|
||||||
|
settings: { ui: { theme: "branded-icon" } },
|
||||||
|
});
|
||||||
|
themes.Set("branded-icon", { name: "branded-icon", title: "Branded", colors: {}, variables: { icon: "logo.svg" } });
|
||||||
|
config.setTheme("branded-icon");
|
||||||
|
expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg");
|
||||||
|
});
|
||||||
|
|
||||||
it("should store values", () => {
|
it("should store values", () => {
|
||||||
const storage = new StorageShim();
|
const storage = new StorageShim();
|
||||||
const values = { siteTitle: "Foo", country: "Germany", city: "Hamburg" };
|
const values = { siteTitle: "Foo", country: "Germany", city: "Hamburg" };
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,37 @@ func TestAuthAny_AppPasswordsDisabled(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusForbidden, s2.HttpStatus())
|
assert.Equal(t, http.StatusForbidden, s2.HttpStatus())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Negative control: the gate keys on IsApplication(), so a non-application
|
||||||
|
// session (here a client-credentials grant) must keep authorizing whether or
|
||||||
|
// not app passwords are disabled, proving the flag does not over-reject.
|
||||||
|
t.Run("NonApplicationSessionUnaffected", func(t *testing.T) {
|
||||||
|
sess, err := entity.AddClientSession("alice-client-cred", conf.SessionMaxAge(), "*", authn.GrantClientCredentials, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, sess.IsApplication())
|
||||||
|
token := sess.AuthToken()
|
||||||
|
|
||||||
|
authPhotos := func() *entity.Session {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/photos", nil)
|
||||||
|
header.SetAuthorization(req, token)
|
||||||
|
req.RemoteAddr = "10.9.8.7:4321"
|
||||||
|
c.Request = req
|
||||||
|
return AuthAny(c, acl.ResourcePhotos, acl.Permissions{acl.ActionView})
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Settings().Features.AppPasswords = true
|
||||||
|
s := authPhotos()
|
||||||
|
require.NotNil(t, s)
|
||||||
|
assert.Equal(t, http.StatusOK, s.HttpStatus())
|
||||||
|
|
||||||
|
conf.Settings().Features.AppPasswords = false
|
||||||
|
s2 := authPhotos()
|
||||||
|
require.NotNil(t, s2)
|
||||||
|
assert.Equal(t, http.StatusOK, s2.HttpStatus(), "disabling app passwords must not reject non-application sessions")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthAny_AppPasswordWebLoginDisabled(t *testing.T) {
|
func TestAuthAny_AppPasswordWebLoginDisabled(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -94,6 +96,32 @@ func TestGetFoldersOriginals(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchFolders_Headers(t *testing.T) {
|
||||||
|
app, router, conf := NewApiTest()
|
||||||
|
_ = conf.CreateDirectories()
|
||||||
|
SearchFoldersOriginals(router)
|
||||||
|
|
||||||
|
// Request uncached results so the count headers are always written
|
||||||
|
// (a cache hit returns early before the headers are added).
|
||||||
|
r := PerformRequest(app, "GET", "/api/v1/folders/originals?uncached=true")
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
result := r.Result()
|
||||||
|
|
||||||
|
xFiles, err := strconv.Atoi(result.Header.Get("X-Files"))
|
||||||
|
assert.NoError(t, err, "X-Files header should be an integer")
|
||||||
|
xFolders, err := strconv.Atoi(result.Header.Get("X-Folders"))
|
||||||
|
assert.NoError(t, err, "X-Folders header should be an integer")
|
||||||
|
xCount, err := strconv.Atoi(result.Header.Get("X-Count"))
|
||||||
|
assert.NoError(t, err, "X-Count header should be an integer")
|
||||||
|
|
||||||
|
// X-Count is the combined number of files and folders returned.
|
||||||
|
assert.Equal(t, xFiles+xFolders, xCount)
|
||||||
|
|
||||||
|
// An unpaginated request reports the form's zero-value count and offset.
|
||||||
|
assert.Equal(t, "0", result.Header.Get("X-Limit"))
|
||||||
|
assert.Equal(t, "0", result.Header.Get("X-Offset"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetFoldersImport(t *testing.T) {
|
func TestGetFoldersImport(t *testing.T) {
|
||||||
t.Run("Flat", func(t *testing.T) {
|
t.Run("Flat", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
@ -1645,10 +1647,21 @@ func TestPhoto_ArchiveRestore(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPhoto_SetCameraSerial(t *testing.T) {
|
func TestPhoto_SetCameraSerial(t *testing.T) {
|
||||||
m := &Photo{}
|
t.Run("Success", func(t *testing.T) {
|
||||||
assert.Empty(t, m.CameraSerial)
|
m := &Photo{}
|
||||||
m.SetCameraSerial("abcCamera")
|
assert.Empty(t, m.CameraSerial)
|
||||||
assert.Equal(t, "abcCamera", m.CameraSerial)
|
m.SetCameraSerial("abcCamera")
|
||||||
|
assert.Equal(t, "abcCamera", m.CameraSerial)
|
||||||
|
})
|
||||||
|
t.Run("MultiByteClippedOnRuneBoundary", func(t *testing.T) {
|
||||||
|
// camera_serial is VARBINARY(160), so a long multi-byte value must be byte-clipped
|
||||||
|
// without splitting a rune, keeping the stored value within budget and valid UTF-8.
|
||||||
|
m := &Photo{}
|
||||||
|
m.SetCameraSerial(strings.Repeat("世", 100)) // 100 runes x 3 bytes = 300 bytes.
|
||||||
|
assert.NotEmpty(t, m.CameraSerial)
|
||||||
|
assert.LessOrEqual(t, len(m.CameraSerial), txt.ClipDefault)
|
||||||
|
assert.True(t, utf8.ValidString(m.CameraSerial))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPhoto_MapKey(t *testing.T) {
|
func TestPhoto_MapKey(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,18 @@ func TestNewLens(t *testing.T) {
|
||||||
assert.Equal(t, "New Model", result.LensModel)
|
assert.Equal(t, "New Model", result.LensModel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLens_Validate(t *testing.T) {
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
frm := &Lens{LensMake: "Canon", LensModel: "EF 50mm f/1.8"}
|
||||||
|
assert.NoError(t, frm.Validate())
|
||||||
|
})
|
||||||
|
t.Run("EmptyMake", func(t *testing.T) {
|
||||||
|
frm := &Lens{LensMake: "", LensModel: "EF 50mm f/1.8"}
|
||||||
|
assert.Error(t, frm.Validate())
|
||||||
|
})
|
||||||
|
t.Run("EmptyModel", func(t *testing.T) {
|
||||||
|
frm := &Lens{LensMake: "Canon", LensModel: ""}
|
||||||
|
assert.Error(t, frm.Validate())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue