fix: hide hidden skills from slug resolution

This commit is contained in:
yun-zhi-ztl 2026-03-17 15:20:35 +08:00
parent 93b5b84d17
commit f0a9714644
6 changed files with 181 additions and 4 deletions

View file

@ -10,7 +10,7 @@ DEV_API_URL := http://localhost:8080
STAGING_API_URL := http://localhost:8080
STAGING_WEB_URL := http://localhost
STAGING_SERVER_IMAGE := skillhub-server:staging
DEV_PROCESS := python3 scripts/dev_process.py
DEV_PROCESS := bash scripts/dev-process.sh
DEV_SERVER_PREPARE := true
DEV_SERVER_CMD := ./scripts/run-dev-app.sh
BACKEND_TEST_JAVA_OPTIONS ?= -XX:+EnableDynamicAgentLoading

156
scripts/dev-process.sh Executable file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage:" >&2
echo " $0 status --pid-file <file>" >&2
echo " $0 stop --pid-file <file>" >&2
echo " $0 start --pid-file <file> --log-file <file> --cwd <dir> -- <command...>" >&2
exit 2
}
require_value() {
local flag="$1"
local value="${2:-}"
if [[ -z "$value" ]]; then
echo "Missing value for $flag" >&2
usage
fi
}
resolve_path() {
local path="$1"
if [[ "$path" = /* ]]; then
printf '%s\n' "$path"
else
printf '%s/%s\n' "$(pwd)" "$path"
fi
}
is_running() {
local pid_file="$1"
[[ -f "$pid_file" ]] || return 1
local pid
pid="$(cat "$pid_file" 2>/dev/null || true)"
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
rm -f "$pid_file"
return 1
}
wait_for_exit() {
local pid="$1"
for _ in $(seq 1 50); do
if ! kill -0 "$pid" 2>/dev/null; then
return 0
fi
sleep 0.1
done
return 1
}
cmd="${1:-}"
[[ -n "$cmd" ]] || usage
shift || true
pid_file=""
log_file=""
cwd=""
case "$cmd" in
status|stop)
while [[ $# -gt 0 ]]; do
case "$1" in
--pid-file)
require_value "$1" "${2:-}"
pid_file="$2"
shift 2
;;
*)
usage
;;
esac
done
[[ -n "$pid_file" ]] || usage
;;
start)
while [[ $# -gt 0 ]]; do
case "$1" in
--pid-file)
require_value "$1" "${2:-}"
pid_file="$2"
shift 2
;;
--log-file)
require_value "$1" "${2:-}"
log_file="$2"
shift 2
;;
--cwd)
require_value "$1" "${2:-}"
cwd="$2"
shift 2
;;
--)
shift
break
;;
*)
usage
;;
esac
done
[[ -n "$pid_file" && -n "$log_file" && -n "$cwd" && $# -gt 0 ]] || usage
;;
*)
usage
;;
esac
case "$cmd" in
status)
is_running "$pid_file"
;;
stop)
if ! is_running "$pid_file"; then
rm -f "$pid_file"
exit 0
fi
pid="$(cat "$pid_file")"
kill "$pid" 2>/dev/null || true
if ! wait_for_exit "$pid"; then
kill -9 "$pid" 2>/dev/null || true
wait_for_exit "$pid" || true
fi
rm -f "$pid_file"
;;
start)
pid_file="$(resolve_path "$pid_file")"
log_file="$(resolve_path "$log_file")"
cwd="$(resolve_path "$cwd")"
mkdir -p "$(dirname "$pid_file")" "$(dirname "$log_file")"
if is_running "$pid_file"; then
echo "Process already running with PID $(cat "$pid_file")" >&2
exit 1
fi
(
cd "$cwd"
if command -v setsid >/dev/null 2>&1; then
setsid "$@" >>"$log_file" 2>&1 < /dev/null &
else
nohup "$@" >>"$log_file" 2>&1 < /dev/null &
fi
child_pid=$!
disown "$child_pid" 2>/dev/null || true
echo "$child_pid" >"$pid_file"
)
;;
esac

View file

@ -95,7 +95,7 @@ class AdminSkillReportControllerTest {
report.setStatus(com.iflytek.skillhub.domain.report.SkillReportStatus.RESOLVED);
when(skillReportService.resolveReport(
org.mockito.ArgumentMatchers.eq(99L),
org.mockito.ArgumentMatchers.eq("admin"),
org.mockito.ArgumentMatchers.eq("super-admin"),
org.mockito.ArgumentMatchers.eq(SkillReportDisposition.RESOLVE_AND_HIDE),
org.mockito.ArgumentMatchers.eq("handled"),
org.mockito.ArgumentMatchers.any(),
@ -103,7 +103,7 @@ class AdminSkillReportControllerTest {
.thenReturn(report);
mockMvc.perform(post("/api/v1/admin/skill-reports/99/resolve")
.with(authentication(adminAuth()))
.with(authentication(superAdminAuth()))
.with(csrf())
.contentType(APPLICATION_JSON)
.content("{\"comment\":\"handled\",\"disposition\":\"RESOLVE_AND_HIDE\"}"))
@ -147,4 +147,13 @@ class AdminSkillReportControllerTest {
principal, null, List.of(new SimpleGrantedAuthority("ROLE_SKILL_ADMIN"))
);
}
private UsernamePasswordAuthenticationToken superAdminAuth() {
PlatformPrincipal principal = new PlatformPrincipal(
"super-admin", "super-admin", "admin@example.com", "", "github", Set.of("SUPER_ADMIN")
);
return new UsernamePasswordAuthenticationToken(
principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))
);
}
}

View file

@ -82,8 +82,10 @@ class SkillSearchAppServiceTest {
void search_shouldFillVisiblePageAcrossArchivedNamespaceResults() {
Skill archivedSkill = new Skill(1L, "archived-skill", "owner-1", SkillVisibility.PUBLIC);
setField(archivedSkill, "id", 10L);
archivedSkill.setLatestVersionId(110L);
Skill visibleSkill = new Skill(2L, "visible-skill", "owner-1", SkillVisibility.PUBLIC);
setField(visibleSkill, "id", 11L);
visibleSkill.setLatestVersionId(111L);
Namespace archivedNamespace = new Namespace("archived-team", "Archived Team", "owner-1");
setField(archivedNamespace, "id", 1L);

View file

@ -32,7 +32,7 @@ public class SkillSlugResolutionService {
? Optional.empty()
: skills.stream().filter(skill -> currentUserId.equals(skill.getOwnerId())).findFirst();
Optional<Skill> publishedSkill = skills.stream()
.filter(skill -> skill.getLatestVersionId() != null)
.filter(skill -> skill.getLatestVersionId() != null && !skill.isHidden())
.findFirst();
if (preference == Preference.CURRENT_USER) {

View file

@ -58,6 +58,16 @@ class SkillSlugResolutionServiceTest {
service.resolve(1L, "demo", null, SkillSlugResolutionService.Preference.CURRENT_USER));
}
@Test
void throwsWhenOnlyPublishedSkillIsHiddenFromCurrentUser() throws Exception {
Skill hiddenPublishedSkill = createSkill(4L, "demo", "user-2", 44L);
hiddenPublishedSkill.setHidden(true);
when(skillRepository.findByNamespaceIdAndSlug(1L, "demo")).thenReturn(List.of(hiddenPublishedSkill));
assertThrows(DomainBadRequestException.class, () ->
service.resolve(1L, "demo", "user-9", SkillSlugResolutionService.Preference.CURRENT_USER));
}
private Skill createSkill(Long id, String slug, String ownerId, Long latestVersionId) throws Exception {
Skill skill = new Skill(1L, slug, ownerId, SkillVisibility.PUBLIC);
Field idField = Skill.class.getDeclaredField("id");