Remove implicit repository routing

415b3cb12270 · AtlantisPleb · · parent 68d395854667

Remove implicit repository routing

Require issue and project operations to carry explicit repository identity, and authorize repository-shaped project routes against that identity.

Claude-Session: 06e35c5f-2f15-4362-b42e-38dd5f015506
Claude-Session
06e35c5f-2f15-4362-b42e-38dd5f015506

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified INVARIANTS.md
  • modified docs/2026-08-21-cli-api-parity-audit.md
  • modified docs/2026-08-21-full-codebase-audit.md
  • modified docs/2026-08-21-issue-project-triage-runbook.md
  • modified docs/github-api-issues-projects-assessment.md
  • modified lib/openagents/issues.ex
  • modified lib/openagents/labels.ex
  • modified lib/openagents/milestones.ex
  • modified lib/openagents/project_items.ex
  • modified lib/openagents/projects.ex
  • modified lib/openagents/repositories.ex
  • modified lib/openagents_web/controllers/issue_controller.ex
  • modified lib/openagents_web/controllers/project_controller.ex
  • modified lib/openagents_web/live/home_live.ex
  • modified lib/openagents_web/live/issue_new_live.ex
  • modified lib/openagents_web/live/issue_show_live.ex
  • modified lib/openagents_web/live/project_index_live.ex
  • modified lib/openagents_web/route_authority.ex
  • modified lib/openagents_web/router.ex
  • modified priv/docs/projects.md
  • modified priv/docs/rest-api.md
  • modified test/openagents/issues_query_test.exs
  • modified test/openagents/issues_test.exs
  • modified test/openagents/labels_test.exs
  • modified test/openagents/milestones_test.exs
  • modified test/openagents/project_fields_test.exs
  • modified test/openagents/project_items_test.exs
  • modified test/openagents/projects_test.exs
  • modified test/openagents/repositories_membership_test.exs
  • modified test/openagents/repositories_test.exs
  • modified test/openagents/scv/codex_runs_test.exs
  • modified test/openagents/staging_cleanup_test.exs
  • modified test/openagents_web/auth_controller_test.exs
  • modified test/openagents_web/controllers/assignee_controller_test.exs
  • modified test/openagents_web/controllers/comment_controller_test.exs
  • modified test/openagents_web/controllers/issue_assignee_controller_test.exs
  • modified test/openagents_web/controllers/issue_controller_test.exs
  • modified test/openagents_web/controllers/issue_label_controller_test.exs
  • modified test/openagents_web/controllers/label_controller_test.exs
  • modified test/openagents_web/controllers/milestone_controller_test.exs
  • modified test/openagents_web/controllers/og_image_controller_test.exs
  • modified test/openagents_web/controllers/project_controller_test.exs
  • modified test/openagents_web/controllers/repository_isolation_controller_test.exs
  • modified test/openagents_web/home_controller_test.exs
  • modified test/openagents_web/live/assignee_index_live_test.exs
  • modified test/openagents_web/live/issue_access_live_test.exs
  • modified test/openagents_web/live/issue_index_live_test.exs
  • modified test/openagents_web/live/issue_new_live_test.exs
  • modified test/openagents_web/live/issue_show_live_test.exs
  • modified test/openagents_web/live/label_index_live_test.exs
  • modified test/openagents_web/live/member_index_live_test.exs
  • modified test/openagents_web/live/milestone_index_live_test.exs
  • modified test/openagents_web/live/project_index_live_test.exs
  • modified test/openagents_web/live/project_show_live_test.exs
  • modified test/openagents_web/route_authority_test.exs
  • modified test/support/conn_case.ex
  • modified test/support/data_case.ex
  • modified test/support/fixtures/accounts_fixtures.ex
  • modified test/support/fixtures/issues_fixtures.ex
  • modified test/support/fixtures/labels_fixtures.ex
  • modified test/support/fixtures/milestones_fixtures.ex
  • modified test/support/fixtures/project_fields_fixtures.ex
  • modified test/support/fixtures/project_items_fixtures.ex
  • modified test/support/fixtures/projects_fixtures.ex

Diff

64 files changed, +1264 -870

INVARIANTS.md modified +12

@@ -1884,11 +1884,23 @@ row from a repository the reader could not open directly. The duplicate that

1884 1884
`list_visible_repositories/1` held had already lost the `ready` half and
1885 1885
disagreed with the paged read about the same repository; there is one copy now.
1886 1886
1887
Amended 2026-08-22 (explicit issue and project repository scope): production
1888
contexts accept a repository or a resource that already carries its repository
1889
identity. No context function selects a default repository or grants membership
1890
as a side effect. The Projects V2 API uses
1891
`/api/v3/repos/:owner/:repo/projectsV2`; every project, item, and field query
1892
includes that repository. Optional bearer authentication lets a member read a
1893
private repository's issues and projects, while anonymous readers and
1894
nonmembers receive `404 Not Found`. Every write still requires `forge:write`
1895
and a writable membership in the same repository.
1896
1887 1897
Evidence: `OpenAgents.Repositories`, `OpenAgents.Repositories.Provisioner`,
1888 1898
`OpenAgents.Repositories.Importer`, `OpenAgents.Forge.GitHTTP`,
1889 1899
`test/openagents/repository_lifecycle_test.exs`,
1890 1900
`test/openagents/repositories/provisioner_test.exs`,
1891 1901
`test/openagents_web/controllers/repository_controller_test.exs`,
1902
`test/openagents_web/controllers/issue_controller_test.exs`,
1903
`test/openagents_web/controllers/project_controller_test.exs`,
1892 1904
`test/openagents/issues_workspace_test.exs`,
1893 1905
`test/openagents_web/live/issue_workspace_live_test.exs`,
1894 1906
`test/openagents_web/live/project_workspace_live_test.exs`, and
docs/2026-08-21-cli-api-parity-audit.md modified +12 -1

@@ -2,7 +2,7 @@

2 2
3 3
**Date:** 2026-08-21
4 4
**Commits measured:** `81e4c25eb5b5` (`openagents/main`, the forge) for the API; `5bd0061e4f6e` in the `openagents` monorepo for `packages/openagents-cli` (package version `0.1.7`)
5
**Status:** Stage 1 shipped on 2026-08-21 as `@openagentsinc/cli@0.2.1`; see section 5. Everything else below describes the surface as measured, unchanged.
5
**Status:** Stages 1, 2, and 7 shipped. Stage 1 added `openagents api` in `@openagentsinc/cli@0.2.1`. Stages 2 and 7 added optional-bearer issue reads and repository-scoped Projects V2 routes on 2026-08-22. Historical measurements below retain the original findings.
6 6
**Question:** Does the CLI only cover repository upload? What of the Issues and Projects API does it reach? Should that coverage be generated from an OpenAPI document instead of hand-written? What is the fastest honest path to managing issues and projects from a terminal?
7 7
**Method:** direct reading of `lib/openagents_web/router.ex`, every controller it routes to under `/api/v3`, the contexts behind them (`lib/openagents/issues.ex`, `labels.ex`, `milestones.ex`, `projects.ex`, `repositories.ex`), the auth plugs, `lib/openagents_web/route_authority.ex`, `priv/api-contracts/repositories-v1.json` and its controller and test, and `docs/openagents-cli/`; plus direct reading of all 21 source files in the `openagents` monorepo at `packages/openagents-cli/src/` and its tests. The CLI lives in a different repository, so every CLI citation names it. Claims that neither repository can settle are in section 7 with the command that would settle them.
8 8

@@ -10,6 +10,12 @@

10 10
11 11
## 0. Summary
12 12
13
> **Update, 2026-08-22:** The server now resolves Projects V2 through
14
> `/repos/:owner/:repo/projectsV2`, applies the named repository's read and
15
> write policy to every project operation, and supports authenticated reads of
16
> private issues. The `initial_repository!/0` and `initial_path/0` shortcuts no
17
> longer exist. The findings below describe the pre-remediation surface.
18
13 19
The owner is right. The CLI is repository-shaped and reaches **none** of the Issues and Projects API.
14 20
15 21
The numbers are clean. The router exposes **50 routes under `/api/v3`** (`lib/openagents_web/router.ex:229`, `:253`, `:259`). The CLI calls **11 of them** — the authenticated user, repository create, list, view, delete, the two import routes, import status, and the two device-authorization routes. Those 11 are exactly the routes named in the hand-written contract document at `priv/api-contracts/repositories-v1.json:18`. The other **39 routes — every issue, comment, label, milestone, assignee, and project endpoint — have no CLI surface at all**, and the CLI's own documentation says so: `docs/openagents-cli/command-reference.md:3` scopes the tool to "authentication and hosted repositories", and `:171` lists a generic API command among the things the release does not provide.

@@ -382,6 +388,11 @@ Ranks 6 through 9. Straightforward once Stage 3 has established the client and c

382 388
383 389
### Stage 7 — Make projects addressable, then build project commands (this repository, then CLI; large)
384 390
391
**Shipped on 2026-08-22.** Projects now use repository-shaped routes and no
392
production context selects a default repository. The generic `openagents api`
393
command can address the resulting project surface; dedicated project commands
394
remain future CLI work.
395
385 396
Unpin the project surface from `Repositories.initial_repository!()` (`projects.ex:26`, `:131`), make `ProjectController.create` honor its own `:owner` segment instead of resolving `Repositories.initial_path()` (`project_controller.ex:21`), reconcile `POST /api/v3/:owner/projectsV2` with the `/users/:username/projectsV2` shape every other project route uses (`router.ex:290`), and give the project reads a visibility predicate. Only then are there project commands worth writing. **Size:** large, and it is genuinely a redesign rather than a port.
386 397
387 398
### What makes issue management usable this week versus complete
docs/2026-08-21-full-codebase-audit.md modified +1 -1

@@ -211,7 +211,7 @@ No dead code beyond Compensation (checked `data_rights`, `diff`, `markdown` —

211 211
1. **The coverage floor gates nothing.** `mix.exs` declares `threshold: 83.0`, and `ops/ci/coverage.sh` correctly unions default and cluster-only passes, but neither `mix precommit` nor `gate.sh` invokes the script. Add a coverage stage to the gate.
212 212
2. **No hosted CI.** Verification depends entirely on developers running the pre-push hook locally; nothing remote proves a push passed the gate. This is documented as deliberate, and the receipt system makes external attestation easy to add later — but today the guarantee is trust-based.
213 213
3. **Contexts without same-name tests.** Verified list includes `Audit`, `Mailer`, `BuildInfo`, `ComputerAgentJobs`, `ReleaseState`, `TurnRecovery` (covered indirectly), among others. `Audit` appears in only one indirect test; `Mailer` has zero test references.
214
4. **A known cross-user authorization gap is pinned as expected behavior.** Per the coverage audit, `ProjectController` ignores the `:username` segment, so a project owned by alice is readable and writable at `/users/bob/projectsV2/:n`. Tests deliberately document the divergence instead of fixing it. Decide: either implement username scoping or remove the segment from the contract.
214
4. **Resolved on 2026-08-22: project routes previously ignored username authority.** Projects now use `/repos/:owner/:repo/projectsV2`, and the repository path controls read and write authority for every project, item, and field operation.
215 215
5. **Test-rule violations.** Seven bare `Process.sleep/1` call sites contradict the repository's own testing rules (`coding_job_test.exs:118`, `computer_tools_test.exs:43`, `registry_and_runner_test.exs:39`, `builder_test.exs:266`, `ra_cluster_test.exs:160` and neighbors). Replace with monitors or the shared `eventually` helper.
216 216
6. **Async tagging is inconsistent**: 76 files tagged `async: true`, 109 tagged false, 48 untagged. Some serialization is likely habitual rather than required; audit and tag deliberately.
217 217
docs/2026-08-21-issue-project-triage-runbook.md modified +3 -5

@@ -213,8 +213,6 @@ destination:

213 213
214 214
```sh
215 215
REPOSITORY_API=https://openagents.com/api/v3/repos/OpenAgentsInc/openagents.com
216
USER_LOGIN=$(curl -fsS "https://openagents.com/api/v3/user" \
217
  -H "Authorization: Bearer $TOKEN" | jq -r '.login')
218 216
219 217
curl -fsS -X POST "$REPOSITORY_API/milestones" \
220 218
  -H "Authorization: Bearer $TOKEN" \

@@ -222,19 +220,19 @@ curl -fsS -X POST "$REPOSITORY_API/milestones" \

222 220
  -d '{"title": "Backlog hygiene", "description": "First triage sweep"}'
223 221
224 222
PROJECT_NUMBER=$(curl -fsS -X POST \
225
  "https://openagents.com/api/v3/$USER_LOGIN/projectsV2" \
223
  "$REPOSITORY_API/projectsV2" \
226 224
  -H "Authorization: Bearer $TOKEN" \
227 225
  -H "Content-Type: application/json" \
228 226
  -d '{"title": "openagents.com roadmap"}' | jq -r '.number')
229 227
230 228
curl -fsS -X POST \
231
  "https://openagents.com/api/v3/users/$USER_LOGIN/projectsV2/$PROJECT_NUMBER/fields" \
229
  "$REPOSITORY_API/projectsV2/$PROJECT_NUMBER/fields" \
232 230
  -H "Authorization: Bearer $TOKEN" \
233 231
  -H "Content-Type: application/json" \
234 232
  -d '{"name": "Status", "data_type": "single_select", "options": {"values": ["To Do", "In Progress", "Done"]}}'
235 233
```
236 234
237
The project belongs to the GitHub login tied to your token. Add high-signal
235
The project belongs to the repository in `REPOSITORY_API`. Add high-signal
238 236
issues to the board and set their `Status` value through the project items API.
239 237
240 238
### Label vocabulary
docs/github-api-issues-projects-assessment.md modified +10 -10

@@ -32,12 +32,11 @@ locks, dependencies, sub-issues, and suggestion APIs are not implemented.

32 32
33 33
| Method | Path |
34 34
| --- | --- |
35
| `GET` | `/users/{username}/projectsV2` |
36
| `POST` | `/{owner}/projectsV2` |
37
| `GET` | `/users/{username}/projectsV2/{project_number}` |
38
| `GET, POST` | `/users/{username}/projectsV2/{project_number}/items` |
39
| `PATCH` | `/users/{username}/projectsV2/{project_number}/items/{item_id}` |
40
| `GET` | `/users/{username}/projectsV2/{project_number}/fields` |
35
| `GET, POST` | `/repos/{owner}/{repo}/projectsV2` |
36
| `GET` | `/repos/{owner}/{repo}/projectsV2/{project_number}` |
37
| `GET, POST` | `/repos/{owner}/{repo}/projectsV2/{project_number}/items` |
38
| `PATCH` | `/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}` |
39
| `GET, POST` | `/repos/{owner}/{repo}/projectsV2/{project_number}/fields` |
41 40
42 41
The project-creation endpoint is an OpenAgents extension because the comparable
43 42
GitHub Projects V2 creation workflow is not supplied by the assessed REST

@@ -48,7 +47,8 @@ ordering, draft items, and organization projects remain unimplemented.

48 47
49 48
These are current measured behaviors:
50 49
51
- `/api/v3` public reads and authenticated writes use separate pipelines.
50
- `/api/v3` anonymous and optional-bearer reads and authenticated writes use
51
  separate pipelines.
52 52
  Writes require an expiring digest-only `oa_pat_…` bearer with exact
53 53
  `forge:write` scope. An authenticated person creates and revokes credentials
54 54
  at `/settings/api-tokens`; plaintext is shown once.

@@ -60,9 +60,9 @@ These are current measured behaviors:

60 60
  assignee links, milestones, and project items.
61 61
- Issue and milestone numbers are repository-local. Project numbers are also
62 62
  repository-local for the repository-shaped LiveView surface.
63
- Project show, item, update-item, and field actions enforce the username in the
64
  route. The user-shaped Projects V2 subset is limited to the explicit initial
65
  repository because those compatibility URLs contain no repository segment.
63
- Project list, show, item, update-item, and field actions resolve the repository
64
  from the route. Public repositories allow anonymous reads. Private reads and
65
  every write require membership in that repository.
66 66
- Assignee reads return active repository members with writable roles, and
67 67
  issue assignment accepts only those members.
68 68
lib/openagents/issues.ex modified +25 -41

@@ -20,10 +20,7 @@ defmodule OpenAgents.Issues do

20 20
  @doc "How many issues one index page shows."
21 21
  def per_page, do: @issues_per_page
22 22
23
  def list_issues(opts \\ []) when is_list(opts),
24
    do: list_issues(Repositories.initial_repository!(), opts)
25
26
  def list_issues(%Repository{id: repository_id}, opts) when is_list(opts) do
23
  def list_issues(%Repository{id: repository_id}, opts \\ []) when is_list(opts) do
27 24
    repository_id
28 25
    |> issue_query(opts)
29 26
    |> order_by([issue], desc: issue.inserted_at, desc: issue.id)

@@ -136,15 +133,10 @@ defmodule OpenAgents.Issues do

136 133
    |> maybe_filter_search(Keyword.get(opts, :q))
137 134
  end
138 135
139
  def get_issue!(id), do: get_issue!(Repositories.initial_repository!(), id)
140
141 136
  def get_issue!(%Repository{id: repository_id}, id) do
142 137
    Repo.get_by!(Issue, id: id, repository_id: repository_id)
143 138
  end
144 139
145
  def get_issue_by_number!(number) when is_integer(number),
146
    do: get_issue_by_number!(Repositories.initial_repository!(), number)
147
148 140
  def get_issue_by_number!(%Repository{id: repository_id}, number) when is_integer(number),
149 141
    do: Repo.get_by!(Issue, repository_id: repository_id, number: number)
150 142

@@ -160,9 +152,6 @@ defmodule OpenAgents.Issues do

160 152
    )
161 153
  end
162 154
163
  def create_issue(attrs \\ %{}),
164
    do: create_issue(Repositories.initial_repository!(), attrs, nil)
165
166 155
  def create_issue(%Repository{} = repository, attrs),
167 156
    do: create_issue(repository, attrs, nil)
168 157

@@ -257,16 +246,13 @@ defmodule OpenAgents.Issues do

257 246
    end
258 247
  end
259 248
260
  def change_issue(%Issue{} = issue, attrs \\ %{}) do
261
    attrs =
262
      if is_nil(issue.repository_id) do
263
        attrs
264
        |> to_string_map()
265
        |> Map.put("repository_id", Repositories.initial_repository!().id)
266
      else
267
        attrs
268
      end
249
  def change_issue(%Repository{id: repository_id}, %Issue{} = issue, attrs) do
250
    attrs = attrs |> to_string_map() |> Map.put("repository_id", repository_id)
251
    Issue.changeset(issue, attrs)
252
  end
269 253
254
  def change_issue(%Issue{repository_id: repository_id} = issue, attrs \\ %{})
255
      when not is_nil(repository_id) do
270 256
    Issue.changeset(issue, attrs)
271 257
  end
272 258

@@ -339,13 +325,6 @@ defmodule OpenAgents.Issues do

339 325
    |> Repo.all()
340 326
  end
341 327
342
  def list_comments(issue_id) do
343
    issue = get_issue!(issue_id)
344
    list_comments(issue)
345
  end
346
347
  def get_comment!(id), do: get_comment!(Repositories.initial_repository!(), id)
348
349 328
  def get_comment!(%Repository{id: repository_id}, id) do
350 329
    Repo.get_by!(Comment, id: id, repository_id: repository_id)
351 330
  end

@@ -362,28 +341,33 @@ defmodule OpenAgents.Issues do

362 341
    )
363 342
  end
364 343
365
  def create_comment(attrs \\ %{}) do
344
  def create_comment(attrs) when is_map(attrs) do
366 345
    normalized = to_string_map(attrs)
367 346
368 347
    case Map.fetch(normalized, "issue_id") do
369 348
      {:ok, issue_id} ->
370
        issue = get_issue!(issue_id)
371
        create_comment(issue, normalized, nil)
349
        case Repo.get(Issue, issue_id) do
350
          %Issue{} = issue -> create_comment(issue, normalized, nil)
351
          nil -> invalid_comment(normalized)
352
        end
372 353
373 354
      :error ->
374
        now = DateTime.utc_now() |> DateTime.truncate(:second)
375
376
        %Comment{}
377
        |> Comment.changeset(
378
          normalized
379
          |> Map.put("repository_id", Repositories.initial_repository!().id)
380
          |> Map.put_new("created_at", now)
381
          |> Map.put_new("updated_at", now)
382
        )
383
        |> Ecto.Changeset.apply_action(:insert)
355
        invalid_comment(normalized)
384 356
    end
385 357
  end
386 358
359
  defp invalid_comment(attrs) do
360
    now = DateTime.utc_now() |> DateTime.truncate(:second)
361
362
    %Comment{}
363
    |> Comment.changeset(
364
      attrs
365
      |> Map.put_new("created_at", now)
366
      |> Map.put_new("updated_at", now)
367
    )
368
    |> Ecto.Changeset.apply_action(:insert)
369
  end
370
387 371
  def create_comment(%Issue{} = issue, attrs, author \\ nil)
388 372
      when is_nil(author) or is_struct(author, User) do
389 373
    normalized =
lib/openagents/labels.ex modified -11

@@ -7,7 +7,6 @@ defmodule OpenAgents.Labels do

7 7
  alias OpenAgents.Accounts.User
8 8
  alias OpenAgents.Analytics
9 9
  alias OpenAgents.Repo
10
  alias OpenAgents.Repositories
11 10
  alias OpenAgents.Repositories.Repository
12 11
13 12
  alias OpenAgents.Labels.Label

@@ -21,8 +20,6 @@ defmodule OpenAgents.Labels do

21 20
      [%Label{}, ...]
22 21
23 22
  """
24
  def list_labels, do: list_labels(Repositories.initial_repository!())
25
26 23
  def list_labels(%Repository{id: repository_id}) do
27 24
    Label
28 25
    |> where(repository_id: ^repository_id)

@@ -44,16 +41,10 @@ defmodule OpenAgents.Labels do

44 41
      ** (Ecto.NoResultsError)
45 42
46 43
  """
47
  def get_label!(id), do: get_label!(Repositories.initial_repository!(), id)
48
49 44
  def get_label!(%Repository{id: repository_id}, id) do
50 45
    Repo.get_by!(Label, id: id, repository_id: repository_id)
51 46
  end
52 47
53
  def get_label_by_name!(name) when is_binary(name) do
54
    get_label_by_name!(Repositories.initial_repository!(), name)
55
  end
56
57 48
  def get_label_by_name!(%Repository{id: repository_id}, name) when is_binary(name) do
58 49
    Repo.get_by!(Label, repository_id: repository_id, name: URI.decode(name))
59 50
  end

@@ -114,8 +105,6 @@ defmodule OpenAgents.Labels do

114 105
      {:error, %Ecto.Changeset{}}
115 106
116 107
  """
117
  def create_label(attrs), do: create_label(Repositories.initial_repository!(), attrs, nil)
118
119 108
  def create_label(%Repository{} = repository, attrs, actor \\ nil)
120 109
      when is_nil(actor) or is_struct(actor, User) do
121 110
    attrs =
lib/openagents/milestones.ex modified -11

@@ -8,7 +8,6 @@ defmodule OpenAgents.Milestones do

8 8
  alias OpenAgents.Analytics
9 9
  alias OpenAgents.Issues.Issue
10 10
  alias OpenAgents.Repo
11
  alias OpenAgents.Repositories
12 11
  alias OpenAgents.Repositories.Repository
13 12
14 13
  alias OpenAgents.Milestones.Milestone

@@ -22,8 +21,6 @@ defmodule OpenAgents.Milestones do

22 21
      [%Milestone{}, ...]
23 22
24 23
  """
25
  def list_milestones, do: list_milestones(Repositories.initial_repository!())
26
27 24
  def list_milestones(%Repository{id: repository_id}) do
28 25
    Milestone
29 26
    |> where(repository_id: ^repository_id)

@@ -46,8 +43,6 @@ defmodule OpenAgents.Milestones do

46 43
      ** (Ecto.NoResultsError)
47 44
48 45
  """
49
  def get_milestone!(id), do: get_milestone!(Repositories.initial_repository!(), id)
50
51 46
  def get_milestone!(%Repository{id: repository_id}, id) do
52 47
    Milestone
53 48
    |> where(id: ^id, repository_id: ^repository_id)

@@ -55,9 +50,6 @@ defmodule OpenAgents.Milestones do

55 50
    |> Repo.one!()
56 51
  end
57 52
58
  def get_milestone_by_number!(number) when is_integer(number),
59
    do: get_milestone_by_number!(Repositories.initial_repository!(), number)
60
61 53
  def get_milestone_by_number!(%Repository{id: repository_id}, number) when is_integer(number) do
62 54
    Milestone
63 55
    |> where(repository_id: ^repository_id, number: ^number)

@@ -92,9 +84,6 @@ defmodule OpenAgents.Milestones do

92 84
      {:error, %Ecto.Changeset{}}
93 85
94 86
  """
95
  def create_milestone(attrs \\ %{}),
96
    do: create_milestone(Repositories.initial_repository!(), attrs, nil)
97
98 87
  def create_milestone(%Repository{} = repository, attrs, actor \\ nil)
99 88
      when is_nil(actor) or is_struct(actor, User) do
100 89
    normalized = for {k, v} <- attrs, into: %{}, do: {to_string(k), v}
lib/openagents/project_items.ex modified +9 -11

@@ -8,7 +8,7 @@ defmodule OpenAgents.ProjectItems do

8 8
9 9
  alias OpenAgents.ProjectItems.ProjectItem
10 10
  alias OpenAgents.Projects.Project
11
  alias OpenAgents.Repositories
11
  alias OpenAgents.Repositories.Repository
12 12
13 13
  @doc """
14 14
  Returns the list of project_items.

@@ -19,8 +19,7 @@ defmodule OpenAgents.ProjectItems do

19 19
      [%ProjectItem{}, ...]
20 20
21 21
  """
22
  def list_project_items do
23
    repository_id = Repositories.initial_repository!().id
22
  def list_project_items(%Repository{id: repository_id}) do
24 23
    Repo.all(from item in ProjectItem, where: item.repository_id == ^repository_id)
25 24
  end
26 25

@@ -38,8 +37,7 @@ defmodule OpenAgents.ProjectItems do

38 37
      ** (Ecto.NoResultsError)
39 38
40 39
  """
41
  def get_project_item!(id) do
42
    repository_id = Repositories.initial_repository!().id
40
  def get_project_item!(%Repository{id: repository_id}, id) do
43 41
    Repo.get_by!(ProjectItem, id: id, repository_id: repository_id)
44 42
  end
45 43

@@ -55,9 +53,9 @@ defmodule OpenAgents.ProjectItems do

55 53
      {:error, %Ecto.Changeset{}}
56 54
57 55
  """
58
  def create_project_item(attrs) do
56
  def create_project_item(%Repository{} = repository, attrs) do
59 57
    attrs = for {key, value} <- attrs, into: %{}, do: {to_string(key), value}
60
    repository_id = repository_id_for(Map.get(attrs, "project_id"))
58
    repository_id = repository_id_for(repository, Map.get(attrs, "project_id"))
61 59
62 60
    %ProjectItem{}
63 61
    |> ProjectItem.changeset(Map.put(attrs, "repository_id", repository_id))

@@ -111,12 +109,12 @@ defmodule OpenAgents.ProjectItems do

111 109
    ProjectItem.changeset(project_item, attrs)
112 110
  end
113 111
114
  defp repository_id_for(nil), do: Repositories.initial_repository!().id
112
  defp repository_id_for(%Repository{id: repository_id}, nil), do: repository_id
115 113
116
  defp repository_id_for(project_id) do
117
    case Repo.get(Project, project_id) do
114
  defp repository_id_for(%Repository{id: repository_id}, project_id) do
115
    case Repo.get_by(Project, id: project_id, repository_id: repository_id) do
118 116
      %Project{repository_id: repository_id} -> repository_id
119
      nil -> Repositories.initial_repository!().id
117
      nil -> repository_id
120 118
    end
121 119
  end
122 120
end
lib/openagents/projects.ex modified +5 -78

@@ -13,8 +13,6 @@ defmodule OpenAgents.Projects do

13 13
  alias OpenAgents.Repositories
14 14
  alias OpenAgents.Repositories.Repository
15 15
16
  def list_projects, do: list_projects(Repositories.initial_repository!())
17
18 16
  def list_projects(%Repository{id: repository_id}) do
19 17
    Project
20 18
    |> where(repository_id: ^repository_id)

@@ -22,18 +20,6 @@ defmodule OpenAgents.Projects do

22 20
    |> Repo.all()
23 21
  end
24 22
25
  def list_projects_by_owner(username) when is_binary(username) do
26
    repository_id = Repositories.initial_repository!().id
27
28
    Repo.all(
29
      from project in Project,
30
        where:
31
          project.repository_id == ^repository_id and
32
            fragment("lower(?)", project.owner) == ^String.downcase(username),
33
        order_by: [asc: project.number]
34
    )
35
  end
36
37 23
  @projects_per_page 25
38 24
39 25
  @doc "How many projects one workspace-wide page shows."

@@ -103,15 +89,10 @@ defmodule OpenAgents.Projects do

103 89
    )
104 90
  end
105 91
106
  def get_project!(id), do: get_project!(Repositories.initial_repository!(), id)
107
108 92
  def get_project!(%Repository{id: repository_id}, id) do
109 93
    Repo.get_by!(Project, id: id, repository_id: repository_id)
110 94
  end
111 95
112
  def get_project_by_number!(number) when is_integer(number),
113
    do: get_project_by_number!(Repositories.initial_repository!(), number)
114
115 96
  def get_project_by_number!(%Repository{id: repository_id}, number) when is_integer(number),
116 97
    do: Repo.get_by!(Project, repository_id: repository_id, number: number)
117 98

@@ -127,21 +108,6 @@ defmodule OpenAgents.Projects do

127 108
    )
128 109
  end
129 110
130
  def get_project_by_owner_and_number!(username, number) when is_integer(number) do
131
    repository_id = Repositories.initial_repository!().id
132
133
    Repo.one!(
134
      from project in Project,
135
        where:
136
          project.repository_id == ^repository_id and
137
            fragment("lower(?)", project.owner) == ^String.downcase(username) and
138
            project.number == ^number
139
    )
140
  end
141
142
  def create_project(attrs \\ %{}),
143
    do: create_project(Repositories.initial_repository!(), attrs, nil)
144
145 111
  def create_project(%Repository{} = repository, attrs),
146 112
    do: create_project(repository, attrs, nil)
147 113

@@ -219,22 +185,14 @@ defmodule OpenAgents.Projects do

219 185
220 186
  def delete_project(%Project{} = project), do: Repo.delete(project)
221 187
222
  def change_project(%Project{} = project, attrs \\ %{}) do
223
    attrs =
224
      if is_nil(project.repository_id) do
225
        attrs
226
        |> to_string_map()
227
        |> Map.put("repository_id", Repositories.initial_repository!().id)
228
      else
229
        attrs
230
      end
231
188
  def change_project(%Repository{id: repository_id}, %Project{} = project, attrs) do
189
    attrs = attrs |> to_string_map() |> Map.put("repository_id", repository_id)
232 190
    Project.changeset(project, attrs)
233 191
  end
234 192
235
  def list_project_items(project_id) when is_integer(project_id) do
236
    project = get_project!(project_id)
237
    list_project_items(project)
193
  def change_project(%Project{repository_id: repository_id} = project, attrs \\ %{})
194
      when not is_nil(repository_id) do
195
    Project.changeset(project, attrs)
238 196
  end
239 197
240 198
  def list_project_items(%Project{id: project_id, repository_id: repository_id}) do

@@ -244,11 +202,6 @@ defmodule OpenAgents.Projects do

244 202
    |> Repo.all()
245 203
  end
246 204
247
  def get_project_item!(id) do
248
    repository = Repositories.initial_repository!()
249
    Repo.get_by!(ProjectItem, id: id, repository_id: repository.id)
250
  end
251
252 205
  def get_project_item!(%Project{id: project_id, repository_id: repository_id}, id) do
253 206
    Repo.get_by!(ProjectItem,
254 207
      id: id,

@@ -257,29 +210,8 @@ defmodule OpenAgents.Projects do

257 210
    )
258 211
  end
259 212
260
  def get_project_item_by_owner!(username, project_number, item_id) do
261
    repository_id = Repositories.initial_repository!().id
262
263
    Repo.one!(
264
      from item in ProjectItem,
265
        join: project in Project,
266
        on:
267
          project.id == item.project_id and project.repository_id == item.repository_id and
268
            project.number == ^project_number,
269
        where:
270
          item.repository_id == ^repository_id and item.id == ^item_id and
271
            fragment("lower(?)", project.owner) == ^String.downcase(username)
272
    )
273
  end
274
275 213
  def create_project_item(attrs, project, actor \\ nil)
276 214
277
  def create_project_item(attrs, project_id, actor)
278
      when is_integer(project_id) do
279
    project = get_project!(project_id)
280
    create_project_item(attrs, project, actor)
281
  end
282
283 215
  def create_project_item(attrs, %Project{} = project, actor)
284 216
      when is_nil(actor) or is_struct(actor, User) do
285 217
    attrs = to_string_map(attrs)

@@ -336,11 +268,6 @@ defmodule OpenAgents.Projects do

336 268
    |> Repo.update()
337 269
  end
338 270
339
  def list_project_fields(project_id) when is_integer(project_id) do
340
    project = get_project!(project_id)
341
    list_project_fields(project)
342
  end
343
344 271
  def list_project_fields(%Project{id: project_id}) do
345 272
    ProjectField
346 273
    |> where(project_id: ^project_id)
lib/openagents/repositories.ex modified -12

@@ -19,8 +19,6 @@ defmodule OpenAgents.Repositories do

19 19
    RepositoryImport
20 20
  }
21 21
22
  @initial_owner "OpenAgentsInc"
23
  @initial_name "openagents.com"
24 22
  @writable_roles ~w(owner maintainer contributor)
25 23
  @all_roles ~w(owner maintainer contributor viewer)
26 24
  @repository_namespace_limit 100

@@ -46,12 +44,6 @@ defmodule OpenAgents.Repositories do

46 44
  # progress or provenance.
47 45
  @provisioning_assocs [:repository_import, :provisioning_outbox]
48 46
49
  def initial_path, do: {@initial_owner, @initial_name}
50
51
  def initial_repository! do
52
    get_by_path!(@initial_owner, @initial_name)
53
  end
54
55 47
  def get_by_path!(owner, name) when is_binary(owner) and is_binary(name) do
56 48
    owner_key = String.downcase(owner)
57 49
    name_key = String.downcase(name)

@@ -763,10 +755,6 @@ defmodule OpenAgents.Repositories do

763 755
764 756
  def machine_access?(%Repository{}, _machine_id, _operation), do: false
765 757
766
  def ensure_initial_membership(%User{} = user) do
767
    add_member(initial_repository!(), user)
768
  end
769
770 758
  def writable?(%Repository{id: repository_id}, %User{id: user_id}) do
771 759
    Repo.exists?(
772 760
      from membership in Membership,
lib/openagents_web/controllers/issue_controller.ex modified +3 -2

@@ -7,7 +7,7 @@ defmodule OpenAgentsWeb.IssueController do

7 7
8 8
  def index(conn, %{"owner" => owner, "repo" => repo} = params) do
9 9
    state = Map.get(params, "state", "open")
10
    repository = Repositories.get_public_by_path!(owner, repo)
10
    repository = Repositories.get_visible_by_path!(owner, repo, conn.assigns[:current_user])
11 11
    issues = Issues.list_issues(repository, state: state)
12 12
    render(conn, :index, issues: issues, owner: owner, repo: repo)
13 13
  rescue

@@ -37,7 +37,8 @@ defmodule OpenAgentsWeb.IssueController do

37 37
        "repo" => repo,
38 38
        "issue_number" => issue_number
39 39
      }) do
40
    issue = Issues.get_issue_by_path!(owner, repo, String.to_integer(issue_number))
40
    repository = Repositories.get_visible_by_path!(owner, repo, conn.assigns[:current_user])
41
    issue = Issues.get_issue_by_number!(repository, String.to_integer(issue_number))
41 42
    render(conn, :show, issue: issue, owner: owner, repo: repo)
42 43
  rescue
43 44
    Ecto.NoResultsError ->
lib/openagents_web/controllers/project_controller.ex modified +61 -81

@@ -5,23 +5,16 @@ defmodule OpenAgentsWeb.ProjectController do

5 5
  alias OpenAgents.Projects.Project
6 6
  alias OpenAgents.Repositories
7 7
8
  def index(conn, %{"username" => username}) do
9
    projects = Projects.list_projects_by_owner(username)
10
11
    render(conn, :index, projects: projects)
8
  def index(conn, %{"owner" => owner, "repo" => repo}) do
9
    repository = visible_repository!(conn, owner, repo)
10
    render(conn, :index, projects: Projects.list_projects(repository))
11
  rescue
12
    Ecto.NoResultsError -> not_found(conn)
12 13
  end
13 14
14
  def create(conn, %{"owner" => owner} = params) do
15
  def create(conn, %{"owner" => owner, "repo" => repo} = params) do
15 16
    user = conn.assigns.current_user
16
17
    if String.downcase(owner) != String.downcase(user.github_login) do
18
      raise Ecto.NoResultsError, queryable: Project
19
    end
20
21
    {repository_owner, repository_name} = Repositories.initial_path()
22
23
    repository =
24
      Repositories.get_writable_by_path!(repository_owner, repository_name, user)
17
    repository = Repositories.get_writable_by_path!(owner, repo, user)
25 18
26 19
    case Projects.create_project(repository, params, user) do
27 20
      {:ok, %Project{} = project} ->

@@ -38,47 +31,42 @@ defmodule OpenAgentsWeb.ProjectController do

38 31
    Ecto.NoResultsError -> not_found(conn)
39 32
  end
40 33
41
  def show(conn, %{"username" => username, "project_number" => project_number}) do
42
    project =
43
      Projects.get_project_by_owner_and_number!(username, String.to_integer(project_number))
44
34
  def show(conn, %{
35
        "owner" => owner,
36
        "repo" => repo,
37
        "project_number" => project_number
38
      }) do
39
    repository = visible_repository!(conn, owner, repo)
40
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
45 41
    render(conn, :show, project: project)
46 42
  rescue
47
    Ecto.NoResultsError ->
48
      conn
49
      |> put_status(:not_found)
50
      |> json(%{message: "Not Found"})
43
    Ecto.NoResultsError -> not_found(conn)
51 44
  end
52 45
53 46
  def items(conn, %{
54
        "username" => username,
47
        "owner" => owner,
48
        "repo" => repo,
55 49
        "project_number" => project_number
56 50
      }) do
57
    project =
58
      Projects.get_project_by_owner_and_number!(username, String.to_integer(project_number))
59
60
    items = Projects.list_project_items(project)
61
    render(conn, :items, items: items)
51
    repository = visible_repository!(conn, owner, repo)
52
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
53
    render(conn, :items, items: Projects.list_project_items(project))
62 54
  rescue
63
    Ecto.NoResultsError ->
64
      conn
65
      |> put_status(:not_found)
66
      |> json(%{message: "Not Found"})
55
    Ecto.NoResultsError -> not_found(conn)
67 56
  end
68 57
69 58
  def create_item(
70 59
        conn,
71 60
        %{
72
          "username" => username,
61
          "owner" => owner,
62
          "repo" => repo,
73 63
          "project_number" => project_number
74 64
        } = params
75 65
      ) do
76
    authorize_owner!(conn.assigns.current_user, username)
77
78
    project =
79
      Projects.get_project_by_owner_and_number!(username, String.to_integer(project_number))
66
    repository = writable_repository!(conn, owner, repo)
67
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
80 68
81
    case cast_issue_number(params["issue_number"]) do
69
    case cast_number(params["issue_number"]) do
82 70
      :error ->
83 71
        unprocessable(conn, %{issue_number: ["is invalid"]})
84 72

@@ -98,28 +86,21 @@ defmodule OpenAgentsWeb.ProjectController do

98 86
        end
99 87
    end
100 88
  rescue
101
    Ecto.NoResultsError ->
102
      conn
103
      |> put_status(:not_found)
104
      |> json(%{message: "Not Found"})
89
    Ecto.NoResultsError -> not_found(conn)
105 90
  end
106 91
107 92
  def update_item(
108 93
        conn,
109 94
        %{
110
          "username" => username,
95
          "owner" => owner,
96
          "repo" => repo,
111 97
          "project_number" => project_number,
112 98
          "item_id" => item_id
113 99
        } = params
114 100
      ) do
115
    authorize_owner!(conn.assigns.current_user, username)
116
117
    item =
118
      Projects.get_project_item_by_owner!(
119
        username,
120
        String.to_integer(project_number),
121
        String.to_integer(item_id)
122
      )
101
    repository = writable_repository!(conn, owner, repo)
102
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
103
    item = Projects.get_project_item!(project, parse_id!(item_id))
123 104
124 105
    if is_map(Map.get(params, "values", %{})) do
125 106
      case Projects.update_project_item(item, params) do

@@ -135,39 +116,31 @@ defmodule OpenAgentsWeb.ProjectController do

135 116
      unprocessable(conn, %{values: ["is invalid"]})
136 117
    end
137 118
  rescue
138
    Ecto.NoResultsError ->
139
      conn
140
      |> put_status(:not_found)
141
      |> json(%{message: "Not Found"})
119
    Ecto.NoResultsError -> not_found(conn)
142 120
  end
143 121
144 122
  def fields(conn, %{
145
        "username" => username,
123
        "owner" => owner,
124
        "repo" => repo,
146 125
        "project_number" => project_number
147 126
      }) do
148
    project =
149
      Projects.get_project_by_owner_and_number!(username, String.to_integer(project_number))
150
151
    fields = Projects.list_project_fields(project)
152
    render(conn, :fields, fields: fields)
127
    repository = visible_repository!(conn, owner, repo)
128
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
129
    render(conn, :fields, fields: Projects.list_project_fields(project))
153 130
  rescue
154
    Ecto.NoResultsError ->
155
      conn
156
      |> put_status(:not_found)
157
      |> json(%{message: "Not Found"})
131
    Ecto.NoResultsError -> not_found(conn)
158 132
  end
159 133
160 134
  def create_field(
161 135
        conn,
162 136
        %{
163
          "username" => username,
137
          "owner" => owner,
138
          "repo" => repo,
164 139
          "project_number" => project_number
165 140
        } = params
166 141
      ) do
167
    authorize_owner!(conn.assigns.current_user, username)
168
169
    project =
170
      Projects.get_project_by_owner_and_number!(username, String.to_integer(project_number))
142
    repository = writable_repository!(conn, owner, repo)
143
    project = Projects.get_project_by_number!(repository, parse_id!(project_number))
171 144
172 145
    attrs =
173 146
      params

@@ -189,16 +162,31 @@ defmodule OpenAgentsWeb.ProjectController do

189 162
    Ecto.NoResultsError -> not_found(conn)
190 163
  end
191 164
192
  defp cast_issue_number(number) when is_integer(number), do: {:ok, number}
165
  defp visible_repository!(conn, owner, repo) do
166
    Repositories.get_visible_by_path!(owner, repo, conn.assigns[:current_user])
167
  end
168
169
  defp writable_repository!(conn, owner, repo) do
170
    Repositories.get_writable_by_path!(owner, repo, conn.assigns.current_user)
171
  end
193 172
194
  defp cast_issue_number(number) when is_binary(number) do
173
  defp parse_id!(value) do
174
    case cast_number(value) do
175
      {:ok, number} -> number
176
      :error -> raise Ecto.NoResultsError, queryable: Project
177
    end
178
  end
179
180
  defp cast_number(number) when is_integer(number), do: {:ok, number}
181
182
  defp cast_number(number) when is_binary(number) do
195 183
    case Integer.parse(number) do
196 184
      {parsed, ""} -> {:ok, parsed}
197 185
      _ -> :error
198 186
    end
199 187
  end
200 188
201
  defp cast_issue_number(_other), do: :error
189
  defp cast_number(_other), do: :error
202 190
203 191
  defp unprocessable(conn, errors) do
204 192
    conn

@@ -206,14 +194,6 @@ defmodule OpenAgentsWeb.ProjectController do

206 194
    |> json(%{errors: errors})
207 195
  end
208 196
209
  defp authorize_owner!(user, username) do
210
    if String.downcase(user.github_login) != String.downcase(username) do
211
      raise Ecto.NoResultsError, queryable: Project
212
    end
213
214
    :ok
215
  end
216
217 197
  defp not_found(conn) do
218 198
    conn |> put_status(:not_found) |> json(%{message: "Not Found"})
219 199
  end
lib/openagents_web/live/home_live.ex modified +11 -36

@@ -60,16 +60,10 @@ defmodule OpenAgentsWeb.HomeLive do

60 60
    |> stream(:repositories, repositories)
61 61
  end
62 62
63
  defp dashboard_repository(repositories) do
64
    {initial_owner, initial_name} = Repositories.initial_path()
63
  defp dashboard_repository(repositories),
64
    do: Enum.find(repositories, &(&1.lifecycle_state == "ready"))
65 65
66
    Enum.find(repositories, fn repository ->
67
      String.downcase(repository.namespace.slug) == String.downcase(initial_owner) and
68
        String.downcase(repository.name) == String.downcase(initial_name)
69
    end) || Enum.find(repositories, &(&1.lifecycle_state == "ready"))
70
  end
71
72
  defp repository_path(nil), do: Repositories.initial_path()
66
  defp repository_path(nil), do: {nil, nil}
73 67
  defp repository_path(repository), do: {repository.namespace.slug, repository.name}
74 68
75 69
  defp list_issues(nil, _state), do: []

@@ -256,33 +250,14 @@ defmodule OpenAgentsWeb.HomeLive do

256 250
          description="Purpose-built for planning and shipping issues. Designed for the agent era."
257 251
        >
258 252
          <:actions>
259
            <%= if @current_user do %>
260
              <.button
261
                id="home-cta-create"
262
                navigate={~p"/OpenAgentsInc/openagents.com/issues/new"}
263
                variant={:primary}
264
                size={:lg}
265
              >
266
                Create new issue
267
              </.button>
268
              <.button
269
                id="home-cta-browse"
270
                navigate={~p"/OpenAgentsInc/openagents.com/issues"}
271
                variant={:secondary}
272
                size={:lg}
273
              >
274
                View issues
275
              </.button>
276
            <% else %>
277
              <.github_login id="home-cta-signin" size={:lg} />
278
              <%!-- Quieter than the action beside it. `variant` defaults to
279
              `:primary`, so two filled buttons sat side by side stating that
280
              both were the thing to do, which leaves a reader picking rather
281
              than proceeding. --%>
282
              <.button navigate={~p"/docs"} variant={:secondary} size={:lg}>
283
                Read the docs
284
              </.button>
285
            <% end %>
253
            <.github_login id="home-cta-signin" size={:lg} />
254
            <%!-- Quieter than the action beside it. `variant` defaults to
255
            `:primary`, so two filled buttons sat side by side stating that
256
            both were the thing to do, which leaves a reader picking rather
257
            than proceeding. --%>
258
            <.button navigate={~p"/docs"} variant={:secondary} size={:lg}>
259
              Read the docs
260
            </.button>
286 261
          </:actions>
287 262
288 263
          <%!-- Commented out rather than deleted: the frame is right, what goes
lib/openagents_web/live/issue_new_live.ex modified +3 -1

@@ -32,7 +32,9 @@ defmodule OpenAgentsWeb.IssueNewLive do

32 32
       |> put_flash(:error, "You cannot open an issue on that repository.")
33 33
       |> redirect(to: ~p"/")}
34 34
    else
35
      changeset = Issues.change_issue(%Issue{}, %{"title" => "", "body" => ""})
35
      changeset =
36
        Issues.change_issue(repository, %Issue{}, %{"title" => "", "body" => ""})
37
36 38
      can_write = Repositories.writable?(repository, user)
37 39
38 40
      {:ok,
lib/openagents_web/live/issue_show_live.ex modified +1 -1

@@ -260,7 +260,7 @@ defmodule OpenAgentsWeb.IssueShowLive do

260 260
    visible? = not is_nil(visible_repository)
261 261
    can_write = visible? and Repositories.writable?(repository, user)
262 262
    can_participate = visible? and Repositories.issue_participant?(repository, user)
263
    issue = Issues.get_issue!(socket.assigns.issue.id)
263
    issue = Issues.get_issue!(socket.assigns.repository, socket.assigns.issue.id)
264 264
    can_edit = can_write || (can_participate and author?(issue, user))
265 265
266 266
    socket
lib/openagents_web/live/project_index_live.ex modified +5 -2

@@ -19,7 +19,7 @@ defmodule OpenAgentsWeb.ProjectIndexLive do

19 19
     |> assign(:repo, repo)
20 20
     |> assign(:repository, repository)
21 21
     |> assign(:projects, Projects.list_projects(repository))
22
     |> assign(:form, to_form(Projects.change_project(%Project{})))}
22
     |> assign(:form, to_form(Projects.change_project(repository, %Project{}, %{})))}
23 23
  end
24 24
25 25
  def handle_event("save", %{"project" => project_params}, socket) do

@@ -32,7 +32,10 @@ defmodule OpenAgentsWeb.ProjectIndexLive do

32 32
        {:noreply,
33 33
         socket
34 34
         |> assign(:projects, Projects.list_projects(socket.assigns.repository))
35
         |> assign(:form, to_form(Projects.change_project(%Project{})))
35
         |> assign(
36
           :form,
37
           to_form(Projects.change_project(socket.assigns.repository, %Project{}, %{}))
38
         )
36 39
         |> put_flash(:info, "Project created")}
37 40
38 41
      {:error, changeset} ->
lib/openagents_web/route_authority.ex modified +19

@@ -54,6 +54,15 @@ defmodule OpenAgentsWeb.RouteAuthority do

54 54
    "/api/computer-agent-jobs/"
55 55
  ]
56 56
57
  @optional_forge_read_paths [
58
    "/api/v3/repos/:owner/:repo/issues",
59
    "/api/v3/repos/:owner/:repo/issues/:issue_number",
60
    "/api/v3/repos/:owner/:repo/projectsV2",
61
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number",
62
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number/items",
63
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number/fields"
64
  ]
65
57 66
  @spec classes() :: [atom()]
58 67
  def classes, do: @classes
59 68

@@ -192,6 +201,16 @@ defmodule OpenAgentsWeb.RouteAuthority do

192 201
        false
193 202
      )
194 203
204
  defp policy(%{path: path, verb: verb})
205
       when path in @optional_forge_read_paths and verb in [:get, :head],
206
       do:
207
         declaration(
208
           :public_read,
209
           "anonymous or first-party bearer token",
210
           "forge:repository:read",
211
           false
212
         )
213
195 214
  defp policy(%{path: "/api/v3/" <> _path, verb: verb}) when verb in [:get, :head],
196 215
    do: declaration(:public_read, "anonymous", "published:forge", false)
197 216
lib/openagents_web/router.ex modified +16 -10

@@ -232,8 +232,6 @@ defmodule OpenAgentsWeb.Router do

232 232
    post "/device/authorizations", DeviceAuthorizationController, :create
233 233
    post "/device/authorizations/token", DeviceAuthorizationController, :token
234 234
235
    get "/repos/:owner/:repo/issues", IssueController, :index
236
    get "/repos/:owner/:repo/issues/:issue_number", IssueController, :show
237 235
    get "/repos/:owner/:repo/issues/:issue_number/comments", CommentController, :index
238 236
    get "/repos/:owner/:repo/issues/comments/:id", CommentController, :show
239 237
    get "/repos/:owner/:repo/issues/:issue_number/labels", IssueLabelController, :index

@@ -244,16 +242,18 @@ defmodule OpenAgentsWeb.Router do

244 242
    get "/repos/:owner/:repo/milestones/:milestone_number", MilestoneController, :show
245 243
    get "/repos/:owner/:repo/assignees", AssigneeController, :index
246 244
    get "/repos/:owner/:repo/assignees/:assignee", AssigneeController, :show
247
    get "/users/:username/projectsV2", ProjectController, :index
248
    get "/users/:username/projectsV2/:project_number", ProjectController, :show
249
    get "/users/:username/projectsV2/:project_number/items", ProjectController, :items
250
    get "/users/:username/projectsV2/:project_number/fields", ProjectController, :fields
251 245
  end
252 246
253 247
  scope "/api/v3", OpenAgentsWeb do
254 248
    pipe_through :optional_forge_api
255 249
256 250
    get "/repos/:owner/:repo", RepositoryController, :show
251
    get "/repos/:owner/:repo/issues", IssueController, :index
252
    get "/repos/:owner/:repo/issues/:issue_number", IssueController, :show
253
    get "/repos/:owner/:repo/projectsV2", ProjectController, :index
254
    get "/repos/:owner/:repo/projectsV2/:project_number", ProjectController, :show
255
    get "/repos/:owner/:repo/projectsV2/:project_number/items", ProjectController, :items
256
    get "/repos/:owner/:repo/projectsV2/:project_number/fields", ProjectController, :fields
257 257
  end
258 258
259 259
  scope "/api/v3", OpenAgentsWeb do

@@ -287,11 +287,17 @@ defmodule OpenAgentsWeb.Router do

287 287
    put "/repos/:owner/:repo/milestones/:milestone_number", MilestoneController, :update
288 288
    patch "/repos/:owner/:repo/milestones/:milestone_number", MilestoneController, :update
289 289
    delete "/repos/:owner/:repo/milestones/:milestone_number", MilestoneController, :delete
290
    post "/:owner/projectsV2", ProjectController, :create
291
    post "/users/:username/projectsV2/:project_number/items", ProjectController, :create_item
292
    post "/users/:username/projectsV2/:project_number/fields", ProjectController, :create_field
290
    post "/repos/:owner/:repo/projectsV2", ProjectController, :create
291
292
    post "/repos/:owner/:repo/projectsV2/:project_number/items",
293
         ProjectController,
294
         :create_item
295
296
    post "/repos/:owner/:repo/projectsV2/:project_number/fields",
297
         ProjectController,
298
         :create_field
293 299
294
    patch "/users/:username/projectsV2/:project_number/items/:item_id",
300
    patch "/repos/:owner/:repo/projectsV2/:project_number/items/:item_id",
295 301
          ProjectController,
296 302
          :update_item
297 303
  end
priv/docs/projects.md modified +3 -8

@@ -15,11 +15,6 @@ issue, so two boards can hold different views of the same work.

15 15
16 16
## Through the API
17 17
18
Projects are exposed under `/users/:username/projectsV2`, shaped after GitHub's
19
ProjectsV2. See [REST API](/docs/rest-api).
20
21
## A current limitation
22
23
The project endpoints do not scope by the `:username` in the path, so a project
24
is reachable under any username. Treat project URLs as unguessable rather than
25
access-controlled until that is closed.
18
Projects are exposed under `/repos/:owner/:repo/projectsV2`. The repository in
19
the path controls visibility and write authority for every project, item, and
20
field operation. See [REST API](/docs/rest-api).
priv/docs/rest-api.md modified +8 -6

@@ -64,10 +64,14 @@ POST /api/v3/repos/:owner/:repo/issues/:issue_number/assignees

64 64
## Projects
65 65
66 66
```
67
GET    /api/v3/users/:username/projectsV2
68
GET    /api/v3/users/:username/projectsV2/:project_number
69
GET    /api/v3/users/:username/projectsV2/:project_number/items
70
GET    /api/v3/users/:username/projectsV2/:project_number/fields
67
GET    /api/v3/repos/:owner/:repo/projectsV2
68
POST   /api/v3/repos/:owner/:repo/projectsV2
69
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number
70
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/items
71
POST   /api/v3/repos/:owner/:repo/projectsV2/:project_number/items
72
PATCH  /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id
73
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields
74
POST   /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields
71 75
```
72 76
73 77
## Known differences from GitHub

@@ -78,8 +82,6 @@ finds them here rather than in production:

78 82
- Renaming a label via `new_name` is accepted and ignored; the path name wins.
79 83
- Applying a label that does not exist returns 404. GitHub creates it.
80 84
- Removing a label an issue does not carry succeeds silently. GitHub returns 404.
81
- The assignable-users endpoint always returns an empty list.
82
- Project endpoints ignore the `:username` in the path.
83 85
- A non-numeric issue, milestone, or project number is a 500 rather than a 404.
84 86
85 87
## What is not implemented
test/openagents/issues_query_test.exs modified +1 -1

@@ -9,7 +9,7 @@ defmodule OpenAgents.IssuesQueryTest do

9 9
  alias OpenAgents.Repositories
10 10
11 11
  setup do
12
    repository = Repositories.initial_repository!()
12
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
13 13
14 14
    {:ok, open} =
15 15
      Issues.create_issue(repository, %{"title" => "First open", "body" => "alpha body"})
test/openagents/issues_test.exs modified +86 -49

@@ -5,13 +5,16 @@ defmodule OpenAgents.IssuesTest do

5 5
  alias OpenAgents.Issues.Comment
6 6
  alias OpenAgents.Issues.Issue
7 7
8
  import OpenAgents.IssuesFixtures
9
  import OpenAgents.LabelsFixtures
10
  import OpenAgents.MilestonesFixtures
11
  import OpenAgents.AccountsFixtures
12
13 8
  setup do
14
    Enum.each(~w(alice bob carol), &repository_user_fixture/1)
9
    repository = repository_fixture()
10
11
    Enum.each(~w(alice bob carol), fn login ->
12
      user = repository_user_fixture(login)
13
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, "contributor")
14
    end)
15
16
    Process.put({__MODULE__, :repository}, repository)
17
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
15 18
    :ok
16 19
  end
17 20

@@ -21,7 +24,7 @@ defmodule OpenAgents.IssuesTest do

21 24
    {1, nil} =
22 25
      Repo.update_all(from(i in Issue, where: i.id == ^issue.id), set: [inserted_at: at])
23 26
24
    Issues.get_issue!(issue.id)
27
    Issues.get_issue!(repository(), issue.id)
25 28
  end
26 29
27 30
  describe "list_issues/1" do

@@ -30,7 +33,7 @@ defmodule OpenAgents.IssuesTest do

30 33
      closed = issue_fixture(title: "closed one")
31 34
      {:ok, closed} = Issues.update_issue(closed, %{"state" => "closed"})
32 35
33
      numbers = Issues.list_issues() |> Enum.map(& &1.number)
36
      numbers = Issues.list_issues(repository()) |> Enum.map(& &1.number)
34 37
35 38
      assert open.number in numbers
36 39
      refute closed.number in numbers

@@ -41,7 +44,9 @@ defmodule OpenAgents.IssuesTest do

41 44
      closed = issue_fixture(title: "closed one")
42 45
      {:ok, closed} = Issues.update_issue(closed, %{"state" => "closed"})
43 46
44
      assert Issues.list_issues(state: "closed") |> Enum.map(& &1.number) == [closed.number]
47
      assert Issues.list_issues(repository(), state: "closed") |> Enum.map(& &1.number) == [
48
               closed.number
49
             ]
45 50
    end
46 51
47 52
    test "state: \"all\" skips the filter" do

@@ -49,13 +54,15 @@ defmodule OpenAgents.IssuesTest do

49 54
      closed = issue_fixture(title: "closed one")
50 55
      {:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
51 56
52
      numbers = Issues.list_issues(state: "all") |> Enum.map(& &1.number) |> Enum.sort()
57
      numbers =
58
        Issues.list_issues(repository(), state: "all") |> Enum.map(& &1.number) |> Enum.sort()
59
53 60
      assert numbers == Enum.sort([open.number, closed.number])
54 61
    end
55 62
56 63
    test "returns an empty list when nothing matches" do
57
      assert Issues.list_issues() == []
58
      assert Issues.list_issues(state: "all") == []
64
      assert Issues.list_issues(repository()) == []
65
      assert Issues.list_issues(repository(), state: "all") == []
59 66
    end
60 67
61 68
    test "orders newest first" do

@@ -63,45 +70,53 @@ defmodule OpenAgents.IssuesTest do

63 70
      middle = issue_fixture(title: "middle") |> backdate!(200)
64 71
      newest = issue_fixture(title: "newest") |> backdate!(100)
65 72
66
      assert Issues.list_issues() |> Enum.map(& &1.id) == [newest.id, middle.id, oldest.id]
73
      assert Issues.list_issues(repository()) |> Enum.map(& &1.id) == [
74
               newest.id,
75
               middle.id,
76
               oldest.id
77
             ]
67 78
    end
68 79
  end
69 80
70 81
  describe "get_issue!/1 and get_issue_by_number!/1" do
71 82
    test "get_issue!/1 returns the issue with the given id" do
72 83
      issue = issue_fixture()
73
      assert Issues.get_issue!(issue.id) == issue
84
      assert Issues.get_issue!(repository(), issue.id) == issue
74 85
    end
75 86
76 87
    test "get_issue!/1 raises for an unknown id" do
77 88
      issue = issue_fixture()
78
      assert_raise Ecto.NoResultsError, fn -> Issues.get_issue!(issue.id + 1) end
89
      assert_raise Ecto.NoResultsError, fn -> Issues.get_issue!(repository(), issue.id + 1) end
79 90
    end
80 91
81 92
    test "get_issue_by_number!/1 returns the issue with the given number" do
82 93
      issue = issue_fixture()
83
      assert Issues.get_issue_by_number!(issue.number) == issue
94
      assert Issues.get_issue_by_number!(repository(), issue.number) == issue
84 95
    end
85 96
86 97
    test "get_issue_by_number!/1 raises for an unknown number" do
87 98
      issue = issue_fixture()
88
      assert_raise Ecto.NoResultsError, fn -> Issues.get_issue_by_number!(issue.number + 1) end
99
100
      assert_raise Ecto.NoResultsError, fn ->
101
        Issues.get_issue_by_number!(repository(), issue.number + 1)
102
      end
89 103
    end
90 104
  end
91 105
92 106
  describe "create_issue/1" do
93 107
    test "assigns numbers from one upwards" do
94
      assert {:ok, %Issue{number: 1}} = Issues.create_issue(%{title: "one"})
95
      assert {:ok, %Issue{number: 2}} = Issues.create_issue(%{title: "two"})
96
      assert {:ok, %Issue{number: 3}} = Issues.create_issue(%{title: "three"})
108
      assert {:ok, %Issue{number: 1}} = Issues.create_issue(repository(), %{title: "one"})
109
      assert {:ok, %Issue{number: 2}} = Issues.create_issue(repository(), %{title: "two"})
110
      assert {:ok, %Issue{number: 3}} = Issues.create_issue(repository(), %{title: "three"})
97 111
    end
98 112
99 113
    test "ignores a caller-supplied number" do
100
      assert {:ok, %Issue{number: 1}} = Issues.create_issue(%{title: "one", number: 99})
114
      assert {:ok, %Issue{number: 1}} =
115
               Issues.create_issue(repository(), %{title: "one", number: 99})
101 116
    end
102 117
103 118
    test "sets the documented defaults" do
104
      assert {:ok, %Issue{} = issue} = Issues.create_issue(%{title: "defaults"})
119
      assert {:ok, %Issue{} = issue} = Issues.create_issue(repository(), %{title: "defaults"})
105 120
106 121
      assert issue.state == "open"
107 122
      assert issue.locked == false

@@ -114,19 +129,21 @@ defmodule OpenAgents.IssuesTest do

114 129
115 130
    test "accepts string keys" do
116 131
      assert {:ok, %Issue{} = issue} =
117
               Issues.create_issue(%{"title" => "strings", "body" => "hello"})
132
               Issues.create_issue(repository(), %{"title" => "strings", "body" => "hello"})
118 133
119 134
      assert issue.title == "strings"
120 135
      assert issue.body == "hello"
121 136
    end
122 137
123 138
    test "requires a title" do
124
      assert {:error, %Ecto.Changeset{} = changeset} = Issues.create_issue(%{body: "no title"})
139
      assert {:error, %Ecto.Changeset{} = changeset} =
140
               Issues.create_issue(repository(), %{body: "no title"})
141
125 142
      assert %{title: ["can't be blank"]} = errors_on(changeset)
126 143
    end
127 144
128 145
    test "called with no attrs at all it still refuses" do
129
      assert {:error, %Ecto.Changeset{} = changeset} = Issues.create_issue()
146
      assert {:error, %Ecto.Changeset{} = changeset} = Issues.create_issue(repository(), %{})
130 147
      assert %{title: ["can't be blank"]} = errors_on(changeset)
131 148
    end
132 149

@@ -134,7 +151,7 @@ defmodule OpenAgents.IssuesTest do

134 151
      label = label_fixture(name: "bug", color: "d73a4a", description: "Something broken")
135 152
136 153
      assert {:ok, %Issue{} = issue} =
137
               Issues.create_issue(%{title: "labelled", labels: ["bug"]})
154
               Issues.create_issue(repository(), %{title: "labelled", labels: ["bug"]})
138 155
139 156
      assert issue.labels == [
140 157
               %{

@@ -148,7 +165,7 @@ defmodule OpenAgents.IssuesTest do

148 165
149 166
    test "rejects a label outside the repository label set" do
150 167
      assert_raise Ecto.NoResultsError, fn ->
151
        Issues.create_issue(%{title: "labelled", labels: ["nope"]})
168
        Issues.create_issue(repository(), %{title: "labelled", labels: ["nope"]})
152 169
      end
153 170
    end
154 171

@@ -156,7 +173,8 @@ defmodule OpenAgents.IssuesTest do

156 173
      label = label_fixture(name: "bug", color: "abcdef")
157 174
      given = [%{"name" => "bug", "color" => "abcdef"}]
158 175
159
      assert {:ok, %Issue{} = issue} = Issues.create_issue(%{title: "labelled", labels: given})
176
      assert {:ok, %Issue{} = issue} =
177
               Issues.create_issue(repository(), %{title: "labelled", labels: given})
160 178
161 179
      assert issue.labels == [
162 180
               %{

@@ -169,12 +187,13 @@ defmodule OpenAgents.IssuesTest do

169 187
    end
170 188
171 189
    test "accepts an empty label list" do
172
      assert {:ok, %Issue{labels: []}} = Issues.create_issue(%{title: "none", labels: []})
190
      assert {:ok, %Issue{labels: []}} =
191
               Issues.create_issue(repository(), %{title: "none", labels: []})
173 192
    end
174 193
175 194
    test "expands assignee logins into assignee maps" do
176 195
      assert {:ok, %Issue{} = issue} =
177
               Issues.create_issue(%{title: "assigned", assignees: ["alice", "bob"]})
196
               Issues.create_issue(repository(), %{title: "assigned", assignees: ["alice", "bob"]})
178 197
179 198
      assert issue.assignees == [%{"login" => "alice"}, %{"login" => "bob"}]
180 199
    end

@@ -182,7 +201,9 @@ defmodule OpenAgents.IssuesTest do

182 201
    test "canonicalizes assignee maps from repository membership" do
183 202
      given = [%{"login" => "alice", "id" => 7}]
184 203
185
      assert {:ok, %Issue{} = issue} = Issues.create_issue(%{title: "assigned", assignees: given})
204
      assert {:ok, %Issue{} = issue} =
205
               Issues.create_issue(repository(), %{title: "assigned", assignees: given})
206
186 207
      assert issue.assignees == [%{"login" => "alice"}]
187 208
    end
188 209

@@ -190,7 +211,7 @@ defmodule OpenAgents.IssuesTest do

190 211
      milestone = milestone_fixture(title: "v1", state: "open", due_on: "2026-01-01")
191 212
192 213
      assert {:ok, %Issue{} = issue} =
193
               Issues.create_issue(%{title: "planned", milestone: milestone.number})
214
               Issues.create_issue(repository(), %{title: "planned", milestone: milestone.number})
194 215
195 216
      assert issue.milestone == %{
196 217
               "number" => milestone.number,

@@ -203,13 +224,13 @@ defmodule OpenAgents.IssuesTest do

203 224
204 225
    test "raises for an unknown milestone number" do
205 226
      assert_raise Ecto.NoResultsError, fn ->
206
        Issues.create_issue(%{title: "planned", milestone: 404})
227
        Issues.create_issue(repository(), %{title: "planned", milestone: 404})
207 228
      end
208 229
    end
209 230
210 231
    test "accepts an explicit nil milestone" do
211 232
      assert {:ok, %Issue{milestone: nil}} =
212
               Issues.create_issue(%{title: "unplanned", milestone: nil})
233
               Issues.create_issue(repository(), %{title: "unplanned", milestone: nil})
213 234
    end
214 235
  end
215 236

@@ -228,7 +249,7 @@ defmodule OpenAgents.IssuesTest do

228 249
      issue = issue_fixture()
229 250
230 251
      assert {:error, %Ecto.Changeset{}} = Issues.update_issue(issue, %{"title" => nil})
231
      assert issue == Issues.get_issue!(issue.id)
252
      assert issue == Issues.get_issue!(repository(), issue.id)
232 253
    end
233 254
234 255
    test "closing stamps closed_at and defaults the state reason" do

@@ -372,7 +393,7 @@ defmodule OpenAgents.IssuesTest do

372 393
373 394
      assert %{color: color} =
374 395
               OpenAgents.Labels.get_label_by_name!(
375
                 OpenAgents.Repositories.initial_repository!(),
396
                 repository(),
376 397
                 "nope"
377 398
               )
378 399

@@ -491,7 +512,7 @@ defmodule OpenAgents.IssuesTest do

491 512
      refute is_nil(comment.created_at)
492 513
      refute is_nil(comment.updated_at)
493 514
494
      assert Issues.get_issue!(issue.id).comments == 1
515
      assert Issues.get_issue!(repository(), issue.id).comments == 1
495 516
    end
496 517
497 518
    test "create_comment/1 keeps explicit timestamps" do

@@ -530,7 +551,7 @@ defmodule OpenAgents.IssuesTest do

530 551
               Issues.create_comment(%{body: nil, issue_id: issue.id})
531 552
532 553
      assert %{body: ["can't be blank"]} = errors_on(changeset)
533
      assert Issues.get_issue!(issue.id).comments == 0
554
      assert Issues.get_issue!(repository(), issue.id).comments == 0
534 555
    end
535 556
536 557
    test "create_comment/1 requires an issue_id" do

@@ -539,7 +560,7 @@ defmodule OpenAgents.IssuesTest do

539 560
    end
540 561
541 562
    test "create_comment/0 refuses an empty comment" do
542
      assert {:error, %Ecto.Changeset{} = changeset} = Issues.create_comment()
563
      assert {:error, %Ecto.Changeset{} = changeset} = Issues.create_comment(%{})
543 564
      assert %{body: ["can't be blank"], issue_id: ["can't be blank"]} = errors_on(changeset)
544 565
    end
545 566

@@ -547,14 +568,16 @@ defmodule OpenAgents.IssuesTest do

547 568
      issue = issue_fixture()
548 569
      {:ok, comment} = Issues.create_comment(%{body: "hello", issue_id: issue.id})
549 570
550
      assert Issues.get_comment!(comment.id) == comment
571
      assert Issues.get_comment!(repository(), comment.id) == comment
551 572
    end
552 573
553 574
    test "get_comment!/1 raises for an unknown id" do
554 575
      issue = issue_fixture()
555 576
      {:ok, comment} = Issues.create_comment(%{body: "hello", issue_id: issue.id})
556 577
557
      assert_raise Ecto.NoResultsError, fn -> Issues.get_comment!(comment.id + 1) end
578
      assert_raise Ecto.NoResultsError, fn ->
579
        Issues.get_comment!(repository(), comment.id + 1)
580
      end
558 581
    end
559 582
560 583
    test "list_comments/1 is scoped to one issue and ordered by creation time" do

@@ -579,12 +602,12 @@ defmodule OpenAgents.IssuesTest do

579 602
580 603
      {:ok, _elsewhere} = Issues.create_comment(%{body: "elsewhere", issue_id: other.id})
581 604
582
      assert Issues.list_comments(issue.id) |> Enum.map(& &1.id) == [first.id, second.id]
605
      assert Issues.list_comments(issue) |> Enum.map(& &1.id) == [first.id, second.id]
583 606
    end
584 607
585 608
    test "list_comments/1 returns an empty list for an issue with no comments" do
586 609
      issue = issue_fixture()
587
      assert Issues.list_comments(issue.id) == []
610
      assert Issues.list_comments(issue) == []
588 611
    end
589 612
590 613
    test "update_comment/2 edits the body and bumps updated_at" do

@@ -610,28 +633,42 @@ defmodule OpenAgents.IssuesTest do

610 633
      {:ok, comment} = Issues.create_comment(%{body: "before", issue_id: issue.id})
611 634
612 635
      assert {:error, %Ecto.Changeset{}} = Issues.update_comment(comment, %{body: nil})
613
      assert Issues.get_comment!(comment.id).body == "before"
636
      assert Issues.get_comment!(repository(), comment.id).body == "before"
614 637
    end
615 638
616 639
    test "delete_comment/1 removes it and decrements the issue counter" do
617 640
      issue = issue_fixture()
618 641
      {:ok, comment} = Issues.create_comment(%{body: "hello", issue_id: issue.id})
619
      assert Issues.get_issue!(issue.id).comments == 1
642
      assert Issues.get_issue!(repository(), issue.id).comments == 1
620 643
621 644
      assert {:ok, :ok} = Issues.delete_comment(comment)
622 645
623
      assert_raise Ecto.NoResultsError, fn -> Issues.get_comment!(comment.id) end
624
      assert Issues.get_issue!(issue.id).comments == 0
646
      assert_raise Ecto.NoResultsError, fn -> Issues.get_comment!(repository(), comment.id) end
647
      assert Issues.get_issue!(repository(), issue.id).comments == 0
625 648
    end
626 649
627 650
    test "the counter tracks several comments" do
628 651
      issue = issue_fixture()
629 652
      {:ok, a} = Issues.create_comment(%{body: "a", issue_id: issue.id})
630 653
      {:ok, _b} = Issues.create_comment(%{body: "b", issue_id: issue.id})
631
      assert Issues.get_issue!(issue.id).comments == 2
654
      assert Issues.get_issue!(repository(), issue.id).comments == 2
632 655
633 656
      {:ok, :ok} = Issues.delete_comment(a)
634
      assert Issues.get_issue!(issue.id).comments == 1
657
      assert Issues.get_issue!(repository(), issue.id).comments == 1
635 658
    end
636 659
  end
660
661
  defp repository, do: Process.get({__MODULE__, :repository})
662
663
  defp issue_fixture(attrs \\ %{}) do
664
    OpenAgents.IssuesFixtures.issue_fixture(repository(), attrs)
665
  end
666
667
  defp label_fixture(attrs) do
668
    OpenAgents.LabelsFixtures.label_fixture(repository(), attrs)
669
  end
670
671
  defp milestone_fixture(attrs) do
672
    OpenAgents.MilestonesFixtures.milestone_fixture(repository(), attrs)
673
  end
637 674
end
test/openagents/labels_test.exs modified +30 -19

@@ -3,6 +3,12 @@ defmodule OpenAgents.LabelsTest do

3 3
4 4
  alias OpenAgents.Labels
5 5
6
  setup do
7
    Process.put({__MODULE__, :repository}, repository_fixture())
8
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
9
    :ok
10
  end
11
6 12
  describe "labels" do
7 13
    alias OpenAgents.Labels.Label
8 14

@@ -11,30 +17,30 @@ defmodule OpenAgents.LabelsTest do

11 17
    @invalid_attrs %{name: nil, description: nil, color: nil}
12 18
13 19
    test "list_labels/0 returns all labels" do
14
      label = label_fixture()
15
      assert Labels.list_labels() == [label]
20
      label = label_fixture(repository())
21
      assert Labels.list_labels(repository()) == [label]
16 22
    end
17 23
18 24
    test "get_label!/1 returns the label with given id" do
19
      label = label_fixture()
20
      assert Labels.get_label!(label.id) == label
25
      label = label_fixture(repository())
26
      assert Labels.get_label!(repository(), label.id) == label
21 27
    end
22 28
23 29
    test "create_label/1 with valid data creates a label" do
24 30
      valid_attrs = %{name: "some name", description: "some description", color: "some color"}
25 31
26
      assert {:ok, %Label{} = label} = Labels.create_label(valid_attrs)
32
      assert {:ok, %Label{} = label} = Labels.create_label(repository(), valid_attrs)
27 33
      assert label.name == "some name"
28 34
      assert label.description == "some description"
29 35
      assert label.color == "some color"
30 36
    end
31 37
32 38
    test "create_label/1 with invalid data returns error changeset" do
33
      assert {:error, %Ecto.Changeset{}} = Labels.create_label(@invalid_attrs)
39
      assert {:error, %Ecto.Changeset{}} = Labels.create_label(repository(), @invalid_attrs)
34 40
    end
35 41
36 42
    test "update_label/2 with valid data updates the label" do
37
      label = label_fixture()
43
      label = label_fixture(repository())
38 44
39 45
      update_attrs = %{
40 46
        name: "some updated name",

@@ -49,24 +55,24 @@ defmodule OpenAgents.LabelsTest do

49 55
    end
50 56
51 57
    test "update_label/2 with invalid data returns error changeset" do
52
      label = label_fixture()
58
      label = label_fixture(repository())
53 59
      assert {:error, %Ecto.Changeset{}} = Labels.update_label(label, @invalid_attrs)
54
      assert label == Labels.get_label!(label.id)
60
      assert label == Labels.get_label!(repository(), label.id)
55 61
    end
56 62
57 63
    test "delete_label/1 deletes the label" do
58
      label = label_fixture()
64
      label = label_fixture(repository())
59 65
      assert {:ok, %Label{}} = Labels.delete_label(label)
60
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label!(label.id) end
66
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label!(repository(), label.id) end
61 67
    end
62 68
63 69
    test "change_label/1 returns a label changeset" do
64
      label = label_fixture()
70
      label = label_fixture(repository())
65 71
      assert %Ecto.Changeset{} = Labels.change_label(label)
66 72
    end
67 73
68 74
    test "change_label/2 applies attrs and surfaces validation errors" do
69
      label = label_fixture()
75
      label = label_fixture(repository())
70 76
71 77
      assert Labels.change_label(label, %{name: "renamed"}).valid?
72 78

@@ -76,18 +82,23 @@ defmodule OpenAgents.LabelsTest do

76 82
    end
77 83
78 84
    test "get_label_by_name!/1 returns the label with the given name" do
79
      label = label_fixture(name: "bug")
80
      assert Labels.get_label_by_name!("bug") == label
85
      label = label_fixture(repository(), name: "bug")
86
      assert Labels.get_label_by_name!(repository(), "bug") == label
81 87
    end
82 88
83 89
    test "get_label_by_name!/1 decodes a percent-encoded name" do
84
      label = label_fixture(name: "help wanted")
85
      assert Labels.get_label_by_name!("help%20wanted") == label
90
      label = label_fixture(repository(), name: "help wanted")
91
      assert Labels.get_label_by_name!(repository(), "help%20wanted") == label
86 92
    end
87 93
88 94
    test "get_label_by_name!/1 raises for an unknown name" do
89
      label_fixture(name: "bug")
90
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label_by_name!("nope") end
95
      label_fixture(repository(), name: "bug")
96
97
      assert_raise Ecto.NoResultsError, fn ->
98
        Labels.get_label_by_name!(repository(), "nope")
99
      end
91 100
    end
92 101
  end
102
103
  defp repository, do: Process.get({__MODULE__, :repository})
93 104
end
test/openagents/milestones_test.exs modified +46 -23

@@ -3,6 +3,12 @@ defmodule OpenAgents.MilestonesTest do

3 3
4 4
  alias OpenAgents.Milestones
5 5
6
  setup do
7
    Process.put({__MODULE__, :repository}, repository_fixture())
8
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
9
    :ok
10
  end
11
6 12
  describe "milestones" do
7 13
    alias OpenAgents.Milestones.Milestone
8 14

@@ -11,13 +17,13 @@ defmodule OpenAgents.MilestonesTest do

11 17
    @invalid_attrs %{state: nil, description: nil, title: nil, number: nil, due_on: nil}
12 18
13 19
    test "list_milestones/0 returns all milestones" do
14
      milestone = milestone_fixture()
15
      assert Milestones.list_milestones() == [milestone]
20
      milestone = milestone_fixture(repository())
21
      assert Milestones.list_milestones(repository()) == [milestone]
16 22
    end
17 23
18 24
    test "get_milestone!/1 returns the milestone with given id" do
19
      milestone = milestone_fixture()
20
      assert Milestones.get_milestone!(milestone.id) == milestone
25
      milestone = milestone_fixture(repository())
26
      assert Milestones.get_milestone!(repository(), milestone.id) == milestone
21 27
    end
22 28
23 29
    test "create_milestone/1 with valid data creates a milestone" do

@@ -29,7 +35,9 @@ defmodule OpenAgents.MilestonesTest do

29 35
        due_on: "some due_on"
30 36
      }
31 37
32
      assert {:ok, %Milestone{} = milestone} = Milestones.create_milestone(valid_attrs)
38
      assert {:ok, %Milestone{} = milestone} =
39
               Milestones.create_milestone(repository(), valid_attrs)
40
33 41
      assert milestone.state == "some state"
34 42
      assert milestone.description == "some description"
35 43
      assert milestone.title == "some title"

@@ -38,11 +46,12 @@ defmodule OpenAgents.MilestonesTest do

38 46
    end
39 47
40 48
    test "create_milestone/1 with invalid data returns error changeset" do
41
      assert {:error, %Ecto.Changeset{}} = Milestones.create_milestone(@invalid_attrs)
49
      assert {:error, %Ecto.Changeset{}} =
50
               Milestones.create_milestone(repository(), @invalid_attrs)
42 51
    end
43 52
44 53
    test "update_milestone/2 updates mutable fields but preserves its repository number" do
45
      milestone = milestone_fixture()
54
      milestone = milestone_fixture(repository())
46 55
47 56
      update_attrs = %{
48 57
        state: "some updated state",

@@ -63,24 +72,27 @@ defmodule OpenAgents.MilestonesTest do

63 72
    end
64 73
65 74
    test "update_milestone/2 with invalid data returns error changeset" do
66
      milestone = milestone_fixture()
75
      milestone = milestone_fixture(repository())
67 76
      assert {:error, %Ecto.Changeset{}} = Milestones.update_milestone(milestone, @invalid_attrs)
68
      assert milestone == Milestones.get_milestone!(milestone.id)
77
      assert milestone == Milestones.get_milestone!(repository(), milestone.id)
69 78
    end
70 79
71 80
    test "delete_milestone/1 deletes the milestone" do
72
      milestone = milestone_fixture()
81
      milestone = milestone_fixture(repository())
73 82
      assert {:ok, %Milestone{}} = Milestones.delete_milestone(milestone)
74
      assert_raise Ecto.NoResultsError, fn -> Milestones.get_milestone!(milestone.id) end
83
84
      assert_raise Ecto.NoResultsError, fn ->
85
        Milestones.get_milestone!(repository(), milestone.id)
86
      end
75 87
    end
76 88
77 89
    test "change_milestone/1 returns a milestone changeset" do
78
      milestone = milestone_fixture()
90
      milestone = milestone_fixture(repository())
79 91
      assert %Ecto.Changeset{} = Milestones.change_milestone(milestone)
80 92
    end
81 93
82 94
    test "change_milestone/2 applies attrs and surfaces validation errors" do
83
      milestone = milestone_fixture()
95
      milestone = milestone_fixture(repository())
84 96
85 97
      assert Milestones.change_milestone(milestone, %{title: "renamed"}).valid?
86 98

@@ -91,41 +103,52 @@ defmodule OpenAgents.MilestonesTest do

91 103
92 104
    test "create_milestone/1 assigns numbers from one upwards" do
93 105
      assert {:ok, %Milestone{number: 1}} =
94
               Milestones.create_milestone(%{title: "v1", state: "open"})
106
               Milestones.create_milestone(repository(), %{title: "v1", state: "open"})
95 107
96 108
      assert {:ok, %Milestone{number: 2}} =
97
               Milestones.create_milestone(%{title: "v2", state: "open"})
109
               Milestones.create_milestone(repository(), %{title: "v2", state: "open"})
98 110
    end
99 111
100 112
    test "create_milestone/1 honours an explicit number and continues from it" do
101 113
      assert {:ok, %Milestone{number: 10}} =
102
               Milestones.create_milestone(%{title: "v10", state: "open", number: 10})
114
               Milestones.create_milestone(repository(), %{
115
                 title: "v10",
116
                 state: "open",
117
                 number: 10
118
               })
103 119
104 120
      assert {:ok, %Milestone{number: 11}} =
105
               Milestones.create_milestone(%{title: "v11", state: "open"})
121
               Milestones.create_milestone(repository(), %{title: "v11", state: "open"})
106 122
    end
107 123
108 124
    test "create_milestone/0 refuses an empty milestone" do
109
      assert {:error, %Ecto.Changeset{} = changeset} = Milestones.create_milestone()
125
      assert {:error, %Ecto.Changeset{} = changeset} =
126
               Milestones.create_milestone(repository(), %{})
127
110 128
      assert %{title: ["can't be blank"]} = errors_on(changeset)
111 129
    end
112 130
113 131
    test "create_milestone/1 accepts string keys" do
114 132
      assert {:ok, %Milestone{} = milestone} =
115
               Milestones.create_milestone(%{"title" => "v1", "state" => "open"})
133
               Milestones.create_milestone(repository(), %{"title" => "v1", "state" => "open"})
116 134
117 135
      assert milestone.title == "v1"
118 136
      assert milestone.state == "open"
119 137
    end
120 138
121 139
    test "get_milestone_by_number!/1 returns the milestone with the given number" do
122
      milestone = milestone_fixture(number: 7)
123
      assert Milestones.get_milestone_by_number!(7) == milestone
140
      milestone = milestone_fixture(repository(), number: 7)
141
      assert Milestones.get_milestone_by_number!(repository(), 7) == milestone
124 142
    end
125 143
126 144
    test "get_milestone_by_number!/1 raises for an unknown number" do
127
      milestone_fixture(number: 7)
128
      assert_raise Ecto.NoResultsError, fn -> Milestones.get_milestone_by_number!(8) end
145
      milestone_fixture(repository(), number: 7)
146
147
      assert_raise Ecto.NoResultsError, fn ->
148
        Milestones.get_milestone_by_number!(repository(), 8)
149
      end
129 150
    end
130 151
  end
152
153
  defp repository, do: Process.get({__MODULE__, :repository})
131 154
end
test/openagents/project_fields_test.exs modified +23 -15

@@ -3,6 +3,12 @@ defmodule OpenAgents.ProjectFieldsTest do

3 3
4 4
  alias OpenAgents.ProjectFields
5 5
6
  setup do
7
    Process.put({__MODULE__, :repository}, repository_fixture())
8
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
9
    :ok
10
  end
11
6 12
  describe "project_fields" do
7 13
    alias OpenAgents.ProjectFields.ProjectField
8 14

@@ -12,7 +18,7 @@ defmodule OpenAgents.ProjectFieldsTest do

12 18
    @invalid_attrs %{name: nil, data_type: nil, options: nil, project_id: nil}
13 19
14 20
    test "list_project_fields/0 returns all project_fields" do
15
      project_field = project_field_fixture()
21
      project_field = project_field_fixture(repository())
16 22
      assert ProjectFields.list_project_fields() == [project_field]
17 23
    end
18 24

@@ -21,20 +27,20 @@ defmodule OpenAgents.ProjectFieldsTest do

21 27
    end
22 28
23 29
    test "list_project_fields/0 spans projects" do
24
      a = project_field_fixture(name: "Status")
25
      b = project_field_fixture(name: "Priority")
30
      a = project_field_fixture(repository(), name: "Status")
31
      b = project_field_fixture(repository(), name: "Priority")
26 32
27 33
      ids = ProjectFields.list_project_fields() |> Enum.map(& &1.id) |> Enum.sort()
28 34
      assert ids == Enum.sort([a.id, b.id])
29 35
    end
30 36
31 37
    test "get_project_field!/1 returns the project_field with given id" do
32
      project_field = project_field_fixture()
38
      project_field = project_field_fixture(repository())
33 39
      assert ProjectFields.get_project_field!(project_field.id) == project_field
34 40
    end
35 41
36 42
    test "get_project_field!/1 raises for an unknown id" do
37
      project_field = project_field_fixture()
43
      project_field = project_field_fixture(repository())
38 44
39 45
      assert_raise Ecto.NoResultsError, fn ->
40 46
        ProjectFields.get_project_field!(project_field.id + 1)

@@ -42,7 +48,7 @@ defmodule OpenAgents.ProjectFieldsTest do

42 48
    end
43 49
44 50
    test "create_project_field/1 with valid data creates a project_field" do
45
      project = project_fixture()
51
      project = project_fixture(repository())
46 52
47 53
      valid_attrs = %{
48 54
        name: "Status",

@@ -61,7 +67,7 @@ defmodule OpenAgents.ProjectFieldsTest do

61 67
    end
62 68
63 69
    test "create_project_field/1 leaves options nil when omitted" do
64
      project = project_fixture()
70
      project = project_fixture(repository())
65 71
66 72
      assert {:ok, %ProjectField{} = project_field} =
67 73
               ProjectFields.create_project_field(%{

@@ -106,7 +112,7 @@ defmodule OpenAgents.ProjectFieldsTest do

106 112
    end
107 113
108 114
    test "update_project_field/2 with valid data updates the project_field" do
109
      project_field = project_field_fixture()
115
      project_field = project_field_fixture(repository())
110 116
111 117
      update_attrs = %{
112 118
        name: "some updated name",

@@ -123,8 +129,8 @@ defmodule OpenAgents.ProjectFieldsTest do

123 129
    end
124 130
125 131
    test "update_project_field/2 can move a field to another project" do
126
      project_field = project_field_fixture()
127
      other = project_fixture(number: 99)
132
      project_field = project_field_fixture(repository())
133
      other = project_fixture(repository(), number: 99)
128 134
129 135
      assert {:ok, %ProjectField{} = moved} =
130 136
               ProjectFields.update_project_field(project_field, %{project_id: other.id})

@@ -133,7 +139,7 @@ defmodule OpenAgents.ProjectFieldsTest do

133 139
    end
134 140
135 141
    test "update_project_field/2 with invalid data returns error changeset" do
136
      project_field = project_field_fixture()
142
      project_field = project_field_fixture(repository())
137 143
138 144
      assert {:error, %Ecto.Changeset{}} =
139 145
               ProjectFields.update_project_field(project_field, @invalid_attrs)

@@ -142,7 +148,7 @@ defmodule OpenAgents.ProjectFieldsTest do

142 148
    end
143 149
144 150
    test "delete_project_field/1 deletes the project_field" do
145
      project_field = project_field_fixture()
151
      project_field = project_field_fixture(repository())
146 152
      assert {:ok, %ProjectField{}} = ProjectFields.delete_project_field(project_field)
147 153
148 154
      assert_raise Ecto.NoResultsError, fn ->

@@ -151,12 +157,12 @@ defmodule OpenAgents.ProjectFieldsTest do

151 157
    end
152 158
153 159
    test "change_project_field/1 returns a project_field changeset" do
154
      project_field = project_field_fixture()
160
      project_field = project_field_fixture(repository())
155 161
      assert %Ecto.Changeset{} = ProjectFields.change_project_field(project_field)
156 162
    end
157 163
158 164
    test "change_project_field/2 applies the given attrs" do
159
      project_field = project_field_fixture()
165
      project_field = project_field_fixture(repository())
160 166
161 167
      changeset = ProjectFields.change_project_field(project_field, %{name: "Renamed"})
162 168

@@ -165,7 +171,7 @@ defmodule OpenAgents.ProjectFieldsTest do

165 171
    end
166 172
167 173
    test "change_project_field/2 surfaces validation errors without touching the database" do
168
      project_field = project_field_fixture()
174
      project_field = project_field_fixture(repository())
169 175
170 176
      changeset = ProjectFields.change_project_field(project_field, %{name: nil})
171 177

@@ -174,4 +180,6 @@ defmodule OpenAgents.ProjectFieldsTest do

174 180
      assert project_field == ProjectFields.get_project_field!(project_field.id)
175 181
    end
176 182
  end
183
184
  defp repository, do: Process.get({__MODULE__, :repository})
177 185
end
test/openagents/project_items_test.exs modified +58 -38

@@ -3,6 +3,12 @@ defmodule OpenAgents.ProjectItemsTest do

3 3
4 4
  alias OpenAgents.ProjectItems
5 5
6
  setup do
7
    Process.put({__MODULE__, :repository}, repository_fixture())
8
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
9
    :ok
10
  end
11
6 12
  describe "project_items" do
7 13
    alias OpenAgents.ProjectItems.ProjectItem
8 14

@@ -13,40 +19,40 @@ defmodule OpenAgents.ProjectItemsTest do

13 19
    @invalid_attrs %{values: nil, project_id: nil, issue_id: nil}
14 20
15 21
    test "list_project_items/0 returns all project_items" do
16
      project_item = project_item_fixture()
17
      assert ProjectItems.list_project_items() == [project_item]
22
      project_item = project_item_fixture(repository())
23
      assert ProjectItems.list_project_items(repository()) == [project_item]
18 24
    end
19 25
20 26
    test "list_project_items/0 returns an empty list when none exist" do
21
      assert ProjectItems.list_project_items() == []
27
      assert ProjectItems.list_project_items(repository()) == []
22 28
    end
23 29
24 30
    test "list_project_items/0 is not scoped to a project" do
25
      a = project_item_fixture()
26
      b = project_item_fixture()
31
      a = project_item_fixture(repository())
32
      b = project_item_fixture(repository())
27 33
28 34
      refute a.project_id == b.project_id
29 35
30
      ids = ProjectItems.list_project_items() |> Enum.map(& &1.id) |> Enum.sort()
36
      ids = ProjectItems.list_project_items(repository()) |> Enum.map(& &1.id) |> Enum.sort()
31 37
      assert ids == Enum.sort([a.id, b.id])
32 38
    end
33 39
34 40
    test "get_project_item!/1 returns the project_item with given id" do
35
      project_item = project_item_fixture()
36
      assert ProjectItems.get_project_item!(project_item.id) == project_item
41
      project_item = project_item_fixture(repository())
42
      assert ProjectItems.get_project_item!(repository(), project_item.id) == project_item
37 43
    end
38 44
39 45
    test "get_project_item!/1 raises for an unknown id" do
40
      project_item = project_item_fixture()
46
      project_item = project_item_fixture(repository())
41 47
42 48
      assert_raise Ecto.NoResultsError, fn ->
43
        ProjectItems.get_project_item!(project_item.id + 1)
49
        ProjectItems.get_project_item!(repository(), project_item.id + 1)
44 50
      end
45 51
    end
46 52
47 53
    test "create_project_item/1 with valid data creates a project_item" do
48
      project = project_fixture()
49
      issue = issue_fixture()
54
      project = project_fixture(repository())
55
      issue = issue_fixture(repository())
50 56
51 57
      valid_attrs = %{
52 58
        values: %{"Status" => "In Progress"},

@@ -55,7 +61,7 @@ defmodule OpenAgents.ProjectItemsTest do

55 61
      }
56 62
57 63
      assert {:ok, %ProjectItem{} = project_item} =
58
               ProjectItems.create_project_item(valid_attrs)
64
               ProjectItems.create_project_item(repository(), valid_attrs)
59 65
60 66
      assert project_item.values == %{"Status" => "In Progress"}
61 67
      assert project_item.project_id == project.id

@@ -63,25 +69,28 @@ defmodule OpenAgents.ProjectItemsTest do

63 69
    end
64 70
65 71
    test "create_project_item/1 leaves values nil when omitted" do
66
      project = project_fixture()
67
      issue = issue_fixture()
72
      project = project_fixture(repository())
73
      issue = issue_fixture(repository())
68 74
69 75
      assert {:ok, %ProjectItem{} = project_item} =
70
               ProjectItems.create_project_item(%{project_id: project.id, issue_id: issue.id})
76
               ProjectItems.create_project_item(repository(), %{
77
                 project_id: project.id,
78
                 issue_id: issue.id
79
               })
71 80
72 81
      assert is_nil(project_item.values)
73 82
    end
74 83
75 84
    test "create_project_item/1 with invalid data returns error changeset" do
76 85
      assert {:error, %Ecto.Changeset{} = changeset} =
77
               ProjectItems.create_project_item(@invalid_attrs)
86
               ProjectItems.create_project_item(repository(), @invalid_attrs)
78 87
79 88
      assert %{project_id: ["can't be blank"], issue_id: ["can't be blank"]} =
80 89
               errors_on(changeset)
81 90
    end
82 91
83 92
    test "create_project_item/1 does not require values" do
84
      assert {:error, changeset} = ProjectItems.create_project_item(%{values: %{}})
93
      assert {:error, changeset} = ProjectItems.create_project_item(repository(), %{values: %{}})
85 94
86 95
      refute Map.has_key?(errors_on(changeset), :values)
87 96

@@ -90,18 +99,20 @@ defmodule OpenAgents.ProjectItemsTest do

90 99
    end
91 100
92 101
    test "create_project_item/1 requires an issue_id even with a project" do
93
      project = project_fixture()
102
      project = project_fixture(repository())
103
104
      assert {:error, changeset} =
105
               ProjectItems.create_project_item(repository(), %{project_id: project.id})
94 106
95
      assert {:error, changeset} = ProjectItems.create_project_item(%{project_id: project.id})
96 107
      assert %{issue_id: ["can't be blank"]} = errors_on(changeset)
97 108
      refute Map.has_key?(errors_on(changeset), :project_id)
98 109
    end
99 110
100 111
    test "create_project_item/1 reports a dangling project as a changeset error" do
101
      issue = issue_fixture()
112
      issue = issue_fixture(repository())
102 113
103 114
      assert {:error, changeset} =
104
               ProjectItems.create_project_item(%{
115
               ProjectItems.create_project_item(repository(), %{
105 116
                 project_id: 2_147_483_000,
106 117
                 issue_id: issue.id
107 118
               })

@@ -110,21 +121,27 @@ defmodule OpenAgents.ProjectItemsTest do

110 121
    end
111 122
112 123
    test "create_project_item/1 allows the same issue in two projects" do
113
      issue = issue_fixture()
114
      one = project_fixture(number: 1)
115
      two = project_fixture(number: 2)
124
      issue = issue_fixture(repository())
125
      one = project_fixture(repository(), number: 1)
126
      two = project_fixture(repository(), number: 2)
116 127
117 128
      assert {:ok, %ProjectItem{}} =
118
               ProjectItems.create_project_item(%{project_id: one.id, issue_id: issue.id})
129
               ProjectItems.create_project_item(repository(), %{
130
                 project_id: one.id,
131
                 issue_id: issue.id
132
               })
119 133
120 134
      assert {:ok, %ProjectItem{}} =
121
               ProjectItems.create_project_item(%{project_id: two.id, issue_id: issue.id})
135
               ProjectItems.create_project_item(repository(), %{
136
                 project_id: two.id,
137
                 issue_id: issue.id
138
               })
122 139
123
      assert length(ProjectItems.list_project_items()) == 2
140
      assert length(ProjectItems.list_project_items(repository())) == 2
124 141
    end
125 142
126 143
    test "update_project_item/2 with valid data updates the project_item" do
127
      project_item = project_item_fixture()
144
      project_item = project_item_fixture(repository())
128 145
      update_attrs = %{values: %{"Status" => "Done"}}
129 146
130 147
      assert {:ok, %ProjectItem{} = project_item} =

@@ -134,7 +151,8 @@ defmodule OpenAgents.ProjectItemsTest do

134 151
    end
135 152
136 153
    test "update_project_item/2 replaces values wholesale rather than merging" do
137
      project_item = project_item_fixture(values: %{"Status" => "Todo", "Size" => "L"})
154
      project_item =
155
        project_item_fixture(repository(), values: %{"Status" => "Todo", "Size" => "L"})
138 156
139 157
      assert {:ok, %ProjectItem{} = updated} =
140 158
               ProjectItems.update_project_item(project_item, %{values: %{"Status" => "Done"}})

@@ -143,30 +161,30 @@ defmodule OpenAgents.ProjectItemsTest do

143 161
    end
144 162
145 163
    test "update_project_item/2 with invalid data returns error changeset" do
146
      project_item = project_item_fixture()
164
      project_item = project_item_fixture(repository())
147 165
148 166
      assert {:error, %Ecto.Changeset{}} =
149 167
               ProjectItems.update_project_item(project_item, @invalid_attrs)
150 168
151
      assert project_item == ProjectItems.get_project_item!(project_item.id)
169
      assert project_item == ProjectItems.get_project_item!(repository(), project_item.id)
152 170
    end
153 171
154 172
    test "delete_project_item/1 deletes the project_item" do
155
      project_item = project_item_fixture()
173
      project_item = project_item_fixture(repository())
156 174
      assert {:ok, %ProjectItem{}} = ProjectItems.delete_project_item(project_item)
157 175
158 176
      assert_raise Ecto.NoResultsError, fn ->
159
        ProjectItems.get_project_item!(project_item.id)
177
        ProjectItems.get_project_item!(repository(), project_item.id)
160 178
      end
161 179
    end
162 180
163 181
    test "change_project_item/1 returns a project_item changeset" do
164
      project_item = project_item_fixture()
182
      project_item = project_item_fixture(repository())
165 183
      assert %Ecto.Changeset{} = ProjectItems.change_project_item(project_item)
166 184
    end
167 185
168 186
    test "change_project_item/2 applies the given attrs" do
169
      project_item = project_item_fixture()
187
      project_item = project_item_fixture(repository())
170 188
171 189
      changeset = ProjectItems.change_project_item(project_item, %{values: %{"Status" => "Done"}})
172 190

@@ -175,13 +193,15 @@ defmodule OpenAgents.ProjectItemsTest do

175 193
    end
176 194
177 195
    test "change_project_item/2 surfaces validation errors without touching the database" do
178
      project_item = project_item_fixture()
196
      project_item = project_item_fixture(repository())
179 197
180 198
      changeset = ProjectItems.change_project_item(project_item, %{issue_id: nil})
181 199
182 200
      refute changeset.valid?
183 201
      assert %{issue_id: ["can't be blank"]} = errors_on(changeset)
184
      assert project_item == ProjectItems.get_project_item!(project_item.id)
202
      assert project_item == ProjectItems.get_project_item!(repository(), project_item.id)
185 203
    end
186 204
  end
205
206
  defp repository, do: Process.get({__MODULE__, :repository})
187 207
end
test/openagents/projects_test.exs modified +69 -54

@@ -3,6 +3,12 @@ defmodule OpenAgents.ProjectsTest do

3 3
4 4
  alias OpenAgents.Projects
5 5
6
  setup do
7
    Process.put({__MODULE__, :repository}, repository_fixture())
8
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
9
    :ok
10
  end
11
6 12
  describe "projects" do
7 13
    alias OpenAgents.Projects.Project
8 14

@@ -11,19 +17,19 @@ defmodule OpenAgents.ProjectsTest do

11 17
    @invalid_attrs %{owner: nil, state: nil, title: nil, number: nil}
12 18
13 19
    test "list_projects/0 returns all projects" do
14
      project = project_fixture()
15
      assert Projects.list_projects() == [project]
20
      project = project_fixture(repository())
21
      assert Projects.list_projects(repository()) == [project]
16 22
    end
17 23
18 24
    test "get_project!/1 returns the project with given id" do
19
      project = project_fixture()
20
      assert Projects.get_project!(project.id) == project
25
      project = project_fixture(repository())
26
      assert Projects.get_project!(repository(), project.id) == project
21 27
    end
22 28
23 29
    test "create_project/1 with valid data creates a project" do
24 30
      valid_attrs = %{owner: "some owner", state: "some state", title: "some title", number: 42}
25 31
26
      assert {:ok, %Project{} = project} = Projects.create_project(valid_attrs)
32
      assert {:ok, %Project{} = project} = Projects.create_project(repository(), valid_attrs)
27 33
      assert project.owner == "some owner"
28 34
      assert project.state == "some state"
29 35
      assert project.title == "some title"

@@ -31,11 +37,11 @@ defmodule OpenAgents.ProjectsTest do

31 37
    end
32 38
33 39
    test "create_project/1 with invalid data returns error changeset" do
34
      assert {:error, %Ecto.Changeset{}} = Projects.create_project(@invalid_attrs)
40
      assert {:error, %Ecto.Changeset{}} = Projects.create_project(repository(), @invalid_attrs)
35 41
    end
36 42
37 43
    test "update_project/2 with valid data updates the project" do
38
      project = project_fixture()
44
      project = project_fixture(repository())
39 45
40 46
      update_attrs = %{
41 47
        owner: "some updated owner",

@@ -52,24 +58,24 @@ defmodule OpenAgents.ProjectsTest do

52 58
    end
53 59
54 60
    test "update_project/2 with invalid data returns error changeset" do
55
      project = project_fixture()
61
      project = project_fixture(repository())
56 62
      assert {:error, %Ecto.Changeset{}} = Projects.update_project(project, @invalid_attrs)
57
      assert project == Projects.get_project!(project.id)
63
      assert project == Projects.get_project!(repository(), project.id)
58 64
    end
59 65
60 66
    test "delete_project/1 deletes the project" do
61
      project = project_fixture()
67
      project = project_fixture(repository())
62 68
      assert {:ok, %Project{}} = Projects.delete_project(project)
63
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project!(project.id) end
69
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project!(repository(), project.id) end
64 70
    end
65 71
66 72
    test "change_project/1 returns a project changeset" do
67
      project = project_fixture()
73
      project = project_fixture(repository())
68 74
      assert %Ecto.Changeset{} = Projects.change_project(project)
69 75
    end
70 76
71 77
    test "change_project/2 applies the given attrs" do
72
      project = project_fixture()
78
      project = project_fixture(repository())
73 79
74 80
      changeset = Projects.change_project(project, %{title: "Renamed"})
75 81

@@ -78,7 +84,7 @@ defmodule OpenAgents.ProjectsTest do

78 84
    end
79 85
80 86
    test "change_project/2 surfaces validation errors" do
81
      project = project_fixture()
87
      project = project_fixture(repository())
82 88
83 89
      changeset = Projects.change_project(project, %{title: nil})
84 90

@@ -88,38 +94,45 @@ defmodule OpenAgents.ProjectsTest do

88 94
89 95
    test "create_project/1 defaults state to open" do
90 96
      assert {:ok, %Project{} = project} =
91
               Projects.create_project(%{title: "Roadmap", owner: "OpenAgents"})
97
               Projects.create_project(repository(), %{title: "Roadmap", owner: "OpenAgents"})
92 98
93 99
      assert project.state == "open"
94 100
    end
95 101
96 102
    test "create_project/1 assigns the next number when none is given" do
97 103
      assert {:ok, %Project{number: 1}} =
98
               Projects.create_project(%{title: "One", owner: "OpenAgents"})
104
               Projects.create_project(repository(), %{title: "One", owner: "OpenAgents"})
99 105
100 106
      assert {:ok, %Project{number: 2}} =
101
               Projects.create_project(%{title: "Two", owner: "OpenAgents"})
107
               Projects.create_project(repository(), %{title: "Two", owner: "OpenAgents"})
102 108
103 109
      assert {:ok, %Project{number: 3}} =
104
               Projects.create_project(%{title: "Three", owner: "OpenAgents"})
110
               Projects.create_project(repository(), %{title: "Three", owner: "OpenAgents"})
105 111
    end
106 112
107 113
    test "create_project/1 honours an explicit number and continues from it" do
108 114
      assert {:ok, %Project{number: 10}} =
109
               Projects.create_project(%{title: "Ten", owner: "OpenAgents", number: 10})
115
               Projects.create_project(repository(), %{
116
                 title: "Ten",
117
                 owner: "OpenAgents",
118
                 number: 10
119
               })
110 120
111 121
      assert {:ok, %Project{number: 11}} =
112
               Projects.create_project(%{title: "Next", owner: "OpenAgents"})
122
               Projects.create_project(repository(), %{title: "Next", owner: "OpenAgents"})
113 123
    end
114 124
115 125
    test "create_project/0 refuses an empty project" do
116
      assert {:error, %Ecto.Changeset{} = changeset} = Projects.create_project()
126
      assert {:error, %Ecto.Changeset{} = changeset} = Projects.create_project(repository(), %{})
117 127
      assert %{title: ["can't be blank"], owner: ["can't be blank"]} = errors_on(changeset)
118 128
    end
119 129
120 130
    test "create_project/1 accepts string keys" do
121 131
      assert {:ok, %Project{} = project} =
122
               Projects.create_project(%{"title" => "Strings", "owner" => "OpenAgents"})
132
               Projects.create_project(repository(), %{
133
                 "title" => "Strings",
134
                 "owner" => "OpenAgents"
135
               })
123 136
124 137
      assert project.title == "Strings"
125 138
      assert project.owner == "OpenAgents"

@@ -127,13 +140,13 @@ defmodule OpenAgents.ProjectsTest do

127 140
    end
128 141
129 142
    test "get_project_by_number!/1 returns the project with the given number" do
130
      project = project_fixture(number: 7)
131
      assert Projects.get_project_by_number!(7) == project
143
      project = project_fixture(repository(), number: 7)
144
      assert Projects.get_project_by_number!(repository(), 7) == project
132 145
    end
133 146
134 147
    test "get_project_by_number!/1 raises for an unknown number" do
135
      project_fixture(number: 7)
136
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project_by_number!(8) end
148
      project_fixture(repository(), number: 7)
149
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project_by_number!(repository(), 8) end
137 150
    end
138 151
  end
139 152

@@ -144,44 +157,44 @@ defmodule OpenAgents.ProjectsTest do

144 157
    import OpenAgents.ProjectsFixtures
145 158
146 159
    setup do
147
      project = project_fixture(number: 1)
148
      issue = issue_fixture(title: "First issue")
160
      project = project_fixture(repository(), number: 1)
161
      issue = issue_fixture(repository(), title: "First issue")
149 162
      %{project: project, issue: issue}
150 163
    end
151 164
152 165
    test "list_project_items/1 is scoped to one project", %{project: project, issue: issue} do
153
      other = project_fixture(number: 2)
166
      other = project_fixture(repository(), number: 2)
154 167
155 168
      {:ok, mine} =
156
        Projects.create_project_item(%{"issue_number" => issue.number}, project.id)
169
        Projects.create_project_item(%{"issue_number" => issue.number}, project)
157 170
158 171
      {:ok, _theirs} =
159
        Projects.create_project_item(%{"issue_number" => issue.number}, other.id)
172
        Projects.create_project_item(%{"issue_number" => issue.number}, other)
160 173
161
      assert Enum.map(Projects.list_project_items(project.id), & &1.id) == [mine.id]
174
      assert Enum.map(Projects.list_project_items(project), & &1.id) == [mine.id]
162 175
    end
163 176
164 177
    test "list_project_items/1 returns an empty list for a project with no items", %{
165 178
      project: project
166 179
    } do
167
      assert Projects.list_project_items(project.id) == []
180
      assert Projects.list_project_items(project) == []
168 181
    end
169 182
170 183
    test "get_project_item!/1 returns the item", %{project: project, issue: issue} do
171
      {:ok, item} = Projects.create_project_item(%{"issue_number" => issue.number}, project.id)
172
      assert Projects.get_project_item!(item.id) == item
184
      {:ok, item} = Projects.create_project_item(%{"issue_number" => issue.number}, project)
185
      assert Projects.get_project_item!(project, item.id) == item
173 186
    end
174 187
175 188
    test "get_project_item!/1 raises for an unknown id", %{project: project, issue: issue} do
176
      {:ok, item} = Projects.create_project_item(%{"issue_number" => issue.number}, project.id)
177
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project_item!(item.id + 1) end
189
      {:ok, item} = Projects.create_project_item(%{"issue_number" => issue.number}, project)
190
      assert_raise Ecto.NoResultsError, fn -> Projects.get_project_item!(project, item.id + 1) end
178 191
    end
179 192
180 193
    test "create_project_item/2 resolves the issue by number", %{project: project, issue: issue} do
181 194
      assert {:ok, %ProjectItem{} = item} =
182 195
               Projects.create_project_item(
183 196
                 %{"issue_number" => issue.number, "values" => %{"Status" => "Todo"}},
184
                 project.id
197
                 project
185 198
               )
186 199
187 200
      assert item.project_id == project.id

@@ -194,7 +207,7 @@ defmodule OpenAgents.ProjectsTest do

194 207
      issue: issue
195 208
    } do
196 209
      assert {:ok, %ProjectItem{} = item} =
197
               Projects.create_project_item(%{"issue_number" => issue.number}, project.id)
210
               Projects.create_project_item(%{"issue_number" => issue.number}, project)
198 211
199 212
      assert item.values == %{}
200 213
    end

@@ -208,7 +221,7 @@ defmodule OpenAgents.ProjectsTest do

208 221
      assert {:ok, %ProjectItem{} = item} =
209 222
               Projects.create_project_item(
210 223
                 %{issue_number: issue.number, values: %{"Status" => "Todo"}},
211
                 project.id
224
                 project
212 225
               )
213 226
214 227
      assert item.issue_id == issue.id

@@ -226,7 +239,7 @@ defmodule OpenAgents.ProjectsTest do

226 239
                   "project_number" => "1",
227 240
                   "issue_number" => issue.number
228 241
                 },
229
                 project.id
242
                 project
230 243
               )
231 244
232 245
      assert item.issue_id == issue.id

@@ -234,7 +247,7 @@ defmodule OpenAgents.ProjectsTest do

234 247
235 248
    test "create_project_item/2 raises when the issue number is unknown", %{project: project} do
236 249
      assert_raise Ecto.NoResultsError, fn ->
237
        Projects.create_project_item(%{"issue_number" => 9999}, project.id)
250
        Projects.create_project_item(%{"issue_number" => 9999}, project)
238 251
      end
239 252
    end
240 253

@@ -244,10 +257,10 @@ defmodule OpenAgents.ProjectsTest do

244 257
      # Regression: a nil issue number used to reach `Repo.get_by!` and raise
245 258
      # ArgumentError, which the /api/v3 controller does not rescue.
246 259
      assert {:error, %Ecto.Changeset{} = changeset} =
247
               Projects.create_project_item(%{"values" => %{}}, project.id)
260
               Projects.create_project_item(%{"values" => %{}}, project)
248 261
249 262
      assert %{issue_id: ["can't be blank"]} = errors_on(changeset)
250
      assert Projects.list_project_items(project.id) == []
263
      assert Projects.list_project_items(project) == []
251 264
    end
252 265
253 266
    test "update_project_item/2 merges into existing values", %{

@@ -257,7 +270,7 @@ defmodule OpenAgents.ProjectsTest do

257 270
      {:ok, item} =
258 271
        Projects.create_project_item(
259 272
          %{"issue_number" => issue.number, "values" => %{"Status" => "Todo", "Size" => "L"}},
260
          project.id
273
          project
261 274
        )
262 275
263 276
      assert {:ok, %ProjectItem{} = updated} =

@@ -270,7 +283,7 @@ defmodule OpenAgents.ProjectsTest do

270 283
      {:ok, item} =
271 284
        Projects.create_project_item(
272 285
          %{"issue_number" => issue.number, "values" => %{"Status" => "Todo"}},
273
          project.id
286
          project
274 287
        )
275 288
276 289
      assert {:ok, %ProjectItem{} = updated} =

@@ -286,7 +299,7 @@ defmodule OpenAgents.ProjectsTest do

286 299
      {:ok, item} =
287 300
        Projects.create_project_item(
288 301
          %{"issue_number" => issue.number, "values" => %{"Status" => "Todo"}},
289
          project.id
302
          project
290 303
        )
291 304
292 305
      assert {:ok, %ProjectItem{} = updated} =

@@ -300,7 +313,7 @@ defmodule OpenAgents.ProjectsTest do

300 313
      issue: issue
301 314
    } do
302 315
      {:ok, item} =
303
        OpenAgents.ProjectItems.create_project_item(%{
316
        OpenAgents.ProjectItems.create_project_item(repository(), %{
304 317
          project_id: project.id,
305 318
          issue_id: issue.id
306 319
        })

@@ -320,8 +333,8 @@ defmodule OpenAgents.ProjectsTest do

320 333
    import OpenAgents.ProjectsFixtures
321 334
322 335
    test "list_project_fields/1 is scoped to one project" do
323
      project = project_fixture(number: 1)
324
      other = project_fixture(number: 2)
336
      project = project_fixture(repository(), number: 1)
337
      other = project_fixture(repository(), number: 2)
325 338
326 339
      {:ok, mine} =
327 340
        Projects.create_project_field(%{

@@ -337,16 +350,16 @@ defmodule OpenAgents.ProjectsTest do

337 350
          project_id: other.id
338 351
        })
339 352
340
      assert Enum.map(Projects.list_project_fields(project.id), & &1.id) == [mine.id]
353
      assert Enum.map(Projects.list_project_fields(project), & &1.id) == [mine.id]
341 354
    end
342 355
343 356
    test "list_project_fields/1 returns an empty list for a project with no fields" do
344
      project = project_fixture()
345
      assert Projects.list_project_fields(project.id) == []
357
      project = project_fixture(repository())
358
      assert Projects.list_project_fields(project) == []
346 359
    end
347 360
348 361
    test "create_project_field/1 with valid data creates a field" do
349
      project = project_fixture()
362
      project = project_fixture(repository())
350 363
351 364
      assert {:ok, %ProjectField{} = field} =
352 365
               Projects.create_project_field(%{

@@ -372,4 +385,6 @@ defmodule OpenAgents.ProjectsTest do

372 385
             } = errors_on(changeset)
373 386
    end
374 387
  end
388
389
  defp repository, do: Process.get({__MODULE__, :repository})
375 390
end
test/openagents/repositories_membership_test.exs modified +1 -1

@@ -23,7 +23,7 @@ defmodule OpenAgents.RepositoriesMembershipTest do

23 23
  end
24 24
25 25
  setup do
26
    repository = Repositories.initial_repository!()
26
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
27 27
    owner = plain_user("members-owner")
28 28
    {:ok, _membership} = Repositories.add_member(repository, owner, "owner")
29 29
test/openagents/repositories_test.exs modified +6 -4

@@ -12,7 +12,7 @@ defmodule OpenAgents.RepositoriesTest do

12 12
  alias OpenAgents.Repositories
13 13
14 14
  setup do
15
    initial = Repositories.initial_repository!()
15
    initial = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
16 16
17 17
    {:ok, second} =
18 18
      Repositories.create_repository(%{

@@ -96,11 +96,11 @@ defmodule OpenAgents.RepositoriesTest do

96 96
  end
97 97
98 98
  test "only active repository members are assignable", %{initial: initial, second: second} do
99
    _initial_only = repository_user_fixture("initial-only")
99
    initial_only = repository_user_fixture("initial-only")
100
    {:ok, _membership} = Repositories.add_member(initial, initial_only, "contributor")
100 101
101 102
    assert Enum.map(Repositories.list_assignable_users(initial), & &1.github_login) == [
102
             "initial-only",
103
             "tenant-member"
103
             "initial-only"
104 104
           ]
105 105
106 106
    assert Enum.map(Repositories.list_assignable_users(second), & &1.github_login) == [

@@ -127,6 +127,7 @@ defmodule OpenAgents.RepositoriesTest do

127 127
    second: second,
128 128
    user: user
129 129
  } do
130
    {:ok, _membership} = Repositories.add_member(initial, user, "maintainer")
130 131
    assert {:ok, project} = Projects.create_project(initial, %{title: "Initial"}, user)
131 132
    assert {:ok, second_issue} = Issues.create_issue(second, %{title: "Second"})
132 133

@@ -162,6 +163,7 @@ defmodule OpenAgents.RepositoriesTest do

162 163
    second: second,
163 164
    user: user
164 165
  } do
166
    {:ok, _membership} = Repositories.add_member(initial, user, "maintainer")
165 167
    assert {:ok, initial_project} = Projects.create_project(initial, %{title: "Initial"}, user)
166 168
    assert {:ok, second_project} = Projects.create_project(second, %{title: "Second"}, user)
167 169
test/openagents/scv/codex_runs_test.exs modified +1 -1

@@ -32,7 +32,7 @@ defmodule OpenAgents.SCV.CodexRunsTest do

32 32
      )
33 33
    )
34 34
35
    repository = Repositories.initial_repository!()
35
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
36 36
    bare = ForgeRepos.ensure_repo!(repository.storage_key, repository.default_branch)
37 37
    source = Path.join(root, "source")
38 38
    File.mkdir_p!(source)
test/openagents/staging_cleanup_test.exs modified +11 -5

@@ -39,7 +39,7 @@ defmodule OpenAgents.StagingCleanupTest do

39 39
  test "one run removes only its registered accounts, repositories, recordings, and machines" do
40 40
    user = repository_user_fixture("cleanup-target")
41 41
    unrelated_user = repository_user_fixture("cleanup-unrelated")
42
    repository = repository_fixture(user)
42
    repository = cleanup_repository_fixture(user)
43 43
    project = project_fixture(repository, user)
44 44
    field = project_field_fixture(project)
45 45
    machine = machine_fixture(user)

@@ -78,7 +78,7 @@ defmodule OpenAgents.StagingCleanupTest do

78 78
    assert Repo.get(Recording, recording.id) == nil
79 79
    assert Repo.get(ProjectField, field.id) == nil
80 80
    assert Repo.get(OpenAgents.Accounts.User, unrelated_user.id)
81
    assert Repositories.initial_repository!()
81
    assert Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
82 82
    assert Repo.aggregate(DisposableResource, :count) == 0
83 83
84 84
    assert {:ok, %{registered: empty}} = StagingCleanup.preview(@run_id)

@@ -87,7 +87,11 @@ defmodule OpenAgents.StagingCleanupTest do

87 87
88 88
  test "registration refuses the canonical repository and administrator accounts" do
89 89
    assert {:error, :canonical_repository_forbidden} =
90
             StagingCleanup.register(@run_id, :repository, Repositories.initial_repository!().id)
90
             StagingCleanup.register(
91
               @run_id,
92
               :repository,
93
               Repositories.get_by_path!("OpenAgentsInc", "openagents.com").id
94
             )
91 95
92 96
    user = repository_user_fixture("cleanup-admin")
93 97
    original_ids = Application.get_env(:openagents, :admin_github_ids)

@@ -100,7 +104,9 @@ defmodule OpenAgents.StagingCleanupTest do

100 104
101 105
  test "cleanup refuses an account that owns a project outside the registered repositories" do
102 106
    user = repository_user_fixture("cleanup-project-owner")
103
    project = project_fixture(Repositories.initial_repository!(), user)
107
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
108
    {:ok, _membership} = Repositories.add_member(repository, user, "owner")
109
    project = project_fixture(repository, user)
104 110
    assert {:ok, _registration} = StagingCleanup.register(@run_id, :account, user.id)
105 111
106 112
    assert {:error, :account_owns_unregistered_project} = StagingCleanup.cleanup(@run_id)

@@ -188,7 +194,7 @@ defmodule OpenAgents.StagingCleanupTest do

188 194
    assert {:error, :staging_cleanup_not_admitted} = StagingCleanup.preview(@run_id)
189 195
  end
190 196
191
  defp repository_fixture(user) do
197
  defp cleanup_repository_fixture(user) do
192 198
    {:ok, repository} =
193 199
      Repositories.create_repository(%{
194 200
        owner: "OpenAgentsStaging",
test/openagents_web/auth_controller_test.exs modified +1 -1

@@ -76,7 +76,7 @@ defmodule OpenAgentsWeb.AuthControllerTest do

76 76
    conn = start_login(conn)
77 77
    {_attempt, state} = attempt_and_state(conn)
78 78
    expect_github(502, "empty-repository-person")
79
    Repositories.initial_repository!() |> Repo.delete!()
79
    Repositories.get_by_path!("OpenAgentsInc", "openagents.com") |> Repo.delete!()
80 80
81 81
    authenticated =
82 82
      conn
test/openagents_web/controllers/assignee_controller_test.exs modified +8 -2

@@ -5,7 +5,8 @@ defmodule OpenAgentsWeb.AssigneeControllerTest do

5 5
6 6
  describe "index" do
7 7
    test "GET /api/v3/repos/:owner/:repo/assignees returns the assignable list", %{conn: conn} do
8
      repository_user_fixture("octocat")
8
      user = repository_user_fixture("octocat")
9
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
9 10
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/assignees")
10 11
11 12
      assert %{"assignees" => [%{"login" => "octocat"}]} = json_response(conn, 200)

@@ -15,7 +16,8 @@ defmodule OpenAgentsWeb.AssigneeControllerTest do

15 16
  describe "show" do
16 17
    test "GET /api/v3/repos/:owner/:repo/assignees/:assignee returns an assignable user",
17 18
         %{conn: conn} do
18
      repository_user_fixture("octocat")
19
      user = repository_user_fixture("octocat")
20
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
19 21
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/assignees/octocat")
20 22
21 23
      assert %{"login" => "octocat"} = json_response(conn, 200)

@@ -27,4 +29,8 @@ defmodule OpenAgentsWeb.AssigneeControllerTest do

27 29
      assert json_response(conn, 404) == %{"message" => "Not Found"}
28 30
    end
29 31
  end
32
33
  defp repository do
34
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
35
  end
30 36
end
test/openagents_web/controllers/comment_controller_test.exs modified +6 -2

@@ -1,12 +1,12 @@

1 1
defmodule OpenAgentsWeb.CommentControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "comments")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "comments", repository())}
5 5
6 6
  alias OpenAgents.Issues
7 7
8 8
  setup do
9
    {:ok, issue} = Issues.create_issue(%{title: "Comment target"})
9
    {:ok, issue} = Issues.create_issue(repository(), %{title: "Comment target"})
10 10
    %{issue: issue}
11 11
  end
12 12

@@ -76,4 +76,8 @@ defmodule OpenAgentsWeb.CommentControllerTest do

76 76
77 77
    assert response(conn, 204)
78 78
  end
79
80
  defp repository do
81
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
82
  end
79 83
end
test/openagents_web/controllers/issue_assignee_controller_test.exs modified +15 -6

@@ -1,15 +1,18 @@

1 1
defmodule OpenAgentsWeb.IssueAssigneeControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issue-assignees")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issue-assignees", repository())}
5 5
6 6
  alias OpenAgents.Issues
7 7
  import OpenAgents.AccountsFixtures
8 8
9 9
  setup do
10
    repository_user_fixture("octocat")
11
    repository_user_fixture("hubot")
12
    {:ok, issue} = Issues.create_issue(%{title: "Assignable issue"})
10
    for login <- ["octocat", "hubot"] do
11
      user = repository_user_fixture(login)
12
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
13
    end
14
15
    {:ok, issue} = Issues.create_issue(repository(), %{title: "Assignable issue"})
13 16
    %{issue: issue}
14 17
  end
15 18

@@ -57,7 +60,8 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

57 60
58 61
      assert %{"assignees" => [%{"login" => "octocat"}]} = json_response(conn, 200)
59 62
60
      assert [%{"login" => "octocat"}] = Issues.get_issue_by_number!(issue.number).assignees
63
      assert [%{"login" => "octocat"}] =
64
               Issues.get_issue_by_number!(repository(), issue.number).assignees
61 65
    end
62 66
63 67
    test "POST .../issues/:issue_number/assignees adds several at once", %{

@@ -135,7 +139,8 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

135 139
136 140
      assert %{"assignees" => [%{"login" => "hubot"}]} = json_response(conn, 200)
137 141
138
      assert [%{"login" => "hubot"}] = Issues.get_issue_by_number!(issue.number).assignees
142
      assert [%{"login" => "hubot"}] =
143
               Issues.get_issue_by_number!(repository(), issue.number).assignees
139 144
    end
140 145
141 146
    test "DELETE .../issues/:issue_number/assignees ignores an assignee that is not set", %{

@@ -167,4 +172,8 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

167 172
      assert json_response(conn, 404) == %{"message" => "Not Found"}
168 173
    end
169 174
  end
175
176
  defp repository do
177
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
178
  end
170 179
end
test/openagents_web/controllers/issue_controller_test.exs modified +51 -6

@@ -1,15 +1,16 @@

1 1
defmodule OpenAgentsWeb.IssueControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issues")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issues", repository())}
5 5
6 6
  alias OpenAgents.Issues
7
  alias OpenAgents.Repositories
7 8
8 9
  describe "index" do
9 10
    test "GET /api/v3/repos/:owner/:repo/issues lists open issues by default", %{
10 11
      conn: conn
11 12
    } do
12
      {:ok, _issue} = Issues.create_issue(%{title: "First issue"})
13
      {:ok, _issue} = Issues.create_issue(repository(), %{title: "First issue"})
13 14
14 15
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
15 16

@@ -19,8 +20,10 @@ defmodule OpenAgentsWeb.IssueControllerTest do

19 20
    end
20 21
21 22
    test "GET /api/v3/repos/:owner/:repo/issues filters by state", %{conn: conn} do
22
      {:ok, _open_issue} = Issues.create_issue(%{title: "Open issue"})
23
      {:ok, _closed_issue} = Issues.create_issue(%{title: "Closed issue", state: "closed"})
23
      {:ok, _open_issue} = Issues.create_issue(repository(), %{title: "Open issue"})
24
25
      {:ok, _closed_issue} =
26
        Issues.create_issue(repository(), %{title: "Closed issue", state: "closed"})
24 27
25 28
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?state=closed")
26 29

@@ -60,7 +63,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

60 63
    test "GET /api/v3/repos/:owner/:repo/issues/:issue_number returns the issue", %{
61 64
      conn: conn
62 65
    } do
63
      {:ok, issue} = Issues.create_issue(%{title: "Show me"})
66
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Show me"})
64 67
65 68
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
66 69

@@ -81,11 +84,49 @@ defmodule OpenAgentsWeb.IssueControllerTest do

81 84
    end
82 85
  end
83 86
87
  describe "private repository reads" do
88
    test "a repository member can list and read issues, but an anonymous visitor cannot" do
89
      private_repository = repository_fixture(%{visibility: "private"})
90
      {:ok, issue} = Issues.create_issue(private_repository, %{title: "Private issue"})
91
92
      path =
93
        "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues"
94
95
      assert get(build_conn(), path) |> json_response(404)
96
97
      member_conn = put_forge_api_token(build_conn(), "private-issue-reader", private_repository)
98
99
      assert %{"issues" => [%{"title" => "Private issue"}]} =
100
               get(member_conn, path) |> json_response(200)
101
102
      issue_path = path <> "/#{issue.number}"
103
104
      member_conn = put_forge_api_token(build_conn(), "private-issue-show", private_repository)
105
      assert %{"title" => "Private issue"} = get(member_conn, issue_path) |> json_response(200)
106
      assert get(build_conn(), issue_path) |> json_response(404)
107
    end
108
109
    test "a bearer without repository membership cannot read private issues" do
110
      private_repository = repository_fixture(%{visibility: "private"})
111
      {:ok, _issue} = Issues.create_issue(private_repository, %{title: "Private issue"})
112
113
      conn = put_forge_api_token(build_conn(), "private-issue-nonmember")
114
115
      conn =
116
        get(
117
          conn,
118
          "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues"
119
        )
120
121
      assert json_response(conn, 404)
122
    end
123
  end
124
84 125
  describe "update" do
85 126
    test "PATCH /api/v3/repos/:owner/:repo/issues/:issue_number closes an issue", %{
86 127
      conn: conn
87 128
    } do
88
      {:ok, issue} = Issues.create_issue(%{title: "Close me"})
129
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Close me"})
89 130
90 131
      conn =
91 132
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{

@@ -96,4 +137,8 @@ defmodule OpenAgentsWeb.IssueControllerTest do

96 137
      assert n == issue.number
97 138
    end
98 139
  end
140
141
  defp repository do
142
    Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
143
  end
99 144
end
test/openagents_web/controllers/issue_label_controller_test.exs modified +24 -17

@@ -1,7 +1,7 @@

1 1
defmodule OpenAgentsWeb.IssueLabelControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issue-labels")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "issue-labels", repository())}
5 5
6 6
  import OpenAgents.LabelsFixtures
7 7

@@ -9,7 +9,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

9 9
  alias OpenAgents.Labels
10 10
11 11
  setup do
12
    {:ok, issue} = Issues.create_issue(%{title: "Labelled issue"})
12
    {:ok, issue} = Issues.create_issue(repository(), %{title: "Labelled issue"})
13 13
    %{issue: issue}
14 14
  end
15 15

@@ -28,7 +28,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

28 28
      conn: conn,
29 29
      issue: issue
30 30
    } do
31
      label_fixture(%{name: "bug", color: "d73a4a"})
31
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
32 32
      {:ok, _issue} = Issues.add_labels(issue, ["bug"])
33 33
34 34
      conn =

@@ -50,7 +50,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

50 50
      conn: conn,
51 51
      issue: issue
52 52
    } do
53
      label_fixture(%{name: "bug", color: "d73a4a"})
53
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
54 54
55 55
      conn =
56 56
        post(

@@ -64,15 +64,15 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

64 64
      assert %{"labels" => [label]} = json_response(conn, 200)
65 65
      assert label["name"] == "bug"
66 66
67
      assert [%{"name" => "bug"}] = Issues.get_issue_by_number!(issue.number).labels
67
      assert [%{"name" => "bug"}] = Issues.get_issue_by_number!(repository(), issue.number).labels
68 68
    end
69 69
70 70
    test "POST .../issues/:issue_number/labels adds several labels at once", %{
71 71
      conn: conn,
72 72
      issue: issue
73 73
    } do
74
      label_fixture(%{name: "bug", color: "d73a4a"})
75
      label_fixture(%{name: "docs", color: "0075ca"})
74
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
75
      label_fixture(repository(), %{name: "docs", color: "0075ca"})
76 76
77 77
      conn =
78 78
        post(

@@ -91,7 +91,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

91 91
      conn: conn,
92 92
      issue: issue
93 93
    } do
94
      label_fixture(%{name: "bug", color: "d73a4a"})
94
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
95 95
      {:ok, _issue} = Issues.add_labels(issue, ["bug"])
96 96
97 97
      conn =

@@ -137,10 +137,12 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

137 137
        )
138 138
139 139
      assert %{"labels" => [%{"name" => "never-created"}]} = json_response(conn, 200)
140
      assert [%{"name" => "never-created"}] = Issues.get_issue_by_number!(issue.number).labels
140
141
      assert [%{"name" => "never-created"}] =
142
               Issues.get_issue_by_number!(repository(), issue.number).labels
141 143
142 144
      refute Labels.get_label_by_name!(
143
               OpenAgents.Repositories.initial_repository!(),
145
               OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
144 146
               "never-created"
145 147
             ).color == ""
146 148
    end

@@ -157,7 +159,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

157 159
        )
158 160
159 161
      assert %{"errors" => %{"name" => [_message]}} = json_response(conn, 422)
160
      assert Issues.get_issue_by_number!(issue.number).labels == []
162
      assert Issues.get_issue_by_number!(repository(), issue.number).labels == []
161 163
    end
162 164
163 165
    test "POST .../issues/:issue_number/labels returns 404 for a missing issue", %{conn: conn} do

@@ -175,8 +177,8 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

175 177
      conn: conn,
176 178
      issue: issue
177 179
    } do
178
      label_fixture(%{name: "bug", color: "d73a4a"})
179
      label_fixture(%{name: "docs", color: "0075ca"})
180
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
181
      label_fixture(repository(), %{name: "docs", color: "0075ca"})
180 182
      {:ok, _issue} = Issues.add_labels(issue, ["bug", "docs"])
181 183
182 184
      conn =

@@ -188,14 +190,15 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

188 190
      assert %{"labels" => [label]} = json_response(conn, 200)
189 191
      assert label["name"] == "docs"
190 192
191
      assert [%{"name" => "docs"}] = Issues.get_issue_by_number!(issue.number).labels
193
      assert [%{"name" => "docs"}] =
194
               Issues.get_issue_by_number!(repository(), issue.number).labels
192 195
    end
193 196
194 197
    test "DELETE .../issues/:issue_number/labels/:name decodes an escaped name", %{
195 198
      conn: conn,
196 199
      issue: issue
197 200
    } do
198
      label_fixture(%{name: "good first issue", color: "7057ff"})
201
      label_fixture(repository(), %{name: "good first issue", color: "7057ff"})
199 202
      {:ok, _issue} = Issues.add_labels(issue, ["good first issue"])
200 203
201 204
      conn =

@@ -214,7 +217,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

214 217
      conn: conn,
215 218
      issue: issue
216 219
    } do
217
      label_fixture(%{name: "bug", color: "d73a4a"})
220
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
218 221
      {:ok, _issue} = Issues.add_labels(issue, ["bug"])
219 222
220 223
      conn =

@@ -224,7 +227,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

224 227
        )
225 228
226 229
      assert json_response(conn, 404)
227
      assert [%{"name" => "bug"}] = Issues.get_issue_by_number!(issue.number).labels
230
      assert [%{"name" => "bug"}] = Issues.get_issue_by_number!(repository(), issue.number).labels
228 231
    end
229 232
230 233
    test "DELETE .../issues/:issue_number/labels/:name returns 404 for a missing issue", %{

@@ -235,4 +238,8 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

235 238
      assert json_response(conn, 404) == %{"message" => "Not Found"}
236 239
    end
237 240
  end
241
242
  defp repository do
243
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
244
  end
238 245
end
test/openagents_web/controllers/label_controller_test.exs modified +22 -18

@@ -1,7 +1,7 @@

1 1
defmodule OpenAgentsWeb.LabelControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "labels")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "labels", repository())}
5 5
6 6
  import OpenAgents.LabelsFixtures
7 7

@@ -9,7 +9,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

9 9
10 10
  describe "index" do
11 11
    test "GET /api/v3/repos/:owner/:repo/labels lists labels", %{conn: conn} do
12
      label_fixture(%{name: "bug", color: "d73a4a"})
12
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
13 13
14 14
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
15 15

@@ -19,7 +19,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

19 19
    end
20 20
21 21
    test "GET /api/v3/repos/:owner/:repo/labels renders a repo-scoped url", %{conn: conn} do
22
      label_fixture(%{name: "bug", color: "d73a4a"})
22
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
23 23
24 24
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
25 25

@@ -52,7 +52,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

52 52
               "default" => false
53 53
             } = json_response(conn, 201)
54 54
55
      assert Labels.get_label_by_name!("enhancement").color == "a2eeef"
55
      assert Labels.get_label_by_name!(repository(), "enhancement").color == "a2eeef"
56 56
    end
57 57
58 58
    test "POST /api/v3/repos/:owner/:repo/labels returns 422 without a color", %{conn: conn} do

@@ -74,7 +74,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

74 74
        color: "ffffff"
75 75
      })
76 76
77
      label = Labels.get_label_by_name!("scoped")
77
      label = Labels.get_label_by_name!(repository(), "scoped")
78 78
      refute Map.has_key?(label, :owner)
79 79
      refute Map.has_key?(label, :repo)
80 80
    end

@@ -82,7 +82,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

82 82
83 83
  describe "show" do
84 84
    test "GET /api/v3/repos/:owner/:repo/labels/:name returns the label", %{conn: conn} do
85
      label = label_fixture(%{name: "bug", color: "d73a4a"})
85
      label = label_fixture(repository(), %{name: "bug", color: "d73a4a"})
86 86
87 87
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug")
88 88

@@ -91,7 +91,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

91 91
    end
92 92
93 93
    test "GET /api/v3/repos/:owner/:repo/labels/:name decodes an escaped name", %{conn: conn} do
94
      label_fixture(%{name: "good first issue", color: "7057ff"})
94
      label_fixture(repository(), %{name: "good first issue", color: "7057ff"})
95 95
96 96
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/good first issue")
97 97

@@ -107,7 +107,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

107 107
    # The advertised URL is percent-encoded the way a path is, so a label
108 108
    # with a space in its name advertises a link that resolves.
109 109
    test "the rendered url round-trips through the show endpoint", %{conn: conn} do
110
      label_fixture(%{name: "good first issue", color: "7057ff"})
110
      label_fixture(repository(), %{name: "good first issue", color: "7057ff"})
111 111
112 112
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
113 113

@@ -121,17 +121,17 @@ defmodule OpenAgentsWeb.LabelControllerTest do

121 121
122 122
  describe "update" do
123 123
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name updates the color", %{conn: conn} do
124
      label_fixture(%{name: "bug", color: "d73a4a"})
124
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
125 125
126 126
      conn =
127 127
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{color: "000000"})
128 128
129 129
      assert %{"name" => "bug", "color" => "000000"} = json_response(conn, 200)
130
      assert Labels.get_label_by_name!("bug").color == "000000"
130
      assert Labels.get_label_by_name!(repository(), "bug").color == "000000"
131 131
    end
132 132
133 133
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name updates the description", %{conn: conn} do
134
      label_fixture(%{name: "bug", color: "d73a4a", description: "old"})
134
      label_fixture(repository(), %{name: "bug", color: "d73a4a", description: "old"})
135 135
136 136
      conn =
137 137
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{

@@ -144,7 +144,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

144 144
    # GitHub renames through `new_name`; the name in the path identifies the
145 145
    # label and `new_name` is what it becomes.
146 146
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name renames with new_name", %{conn: conn} do
147
      label_fixture(%{name: "bug", color: "d73a4a"})
147
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
148 148
149 149
      conn =
150 150
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{

@@ -152,15 +152,15 @@ defmodule OpenAgentsWeb.LabelControllerTest do

152 152
        })
153 153
154 154
      assert json_response(conn, 200)["name"] == "defect"
155
      assert Labels.get_label_by_name!("defect").color == "d73a4a"
155
      assert Labels.get_label_by_name!(repository(), "defect").color == "d73a4a"
156 156
157
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label_by_name!("bug") end
157
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label_by_name!(repository(), "bug") end
158 158
    end
159 159
160 160
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name returns 422 for a blank color", %{
161 161
      conn: conn
162 162
    } do
163
      label_fixture(%{name: "bug", color: "d73a4a"})
163
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
164 164
165 165
      conn =
166 166
        conn

@@ -168,7 +168,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

168 168
        |> patch(~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", ~s({"color": null}))
169 169
170 170
      assert %{"errors" => %{"color" => _}} = json_response(conn, 422)
171
      assert Labels.get_label_by_name!("bug").color == "d73a4a"
171
      assert Labels.get_label_by_name!(repository(), "bug").color == "d73a4a"
172 172
    end
173 173
174 174
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do

@@ -181,12 +181,12 @@ defmodule OpenAgentsWeb.LabelControllerTest do

181 181
182 182
  describe "delete" do
183 183
    test "DELETE /api/v3/repos/:owner/:repo/labels/:name returns 204", %{conn: conn} do
184
      label_fixture(%{name: "bug", color: "d73a4a"})
184
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
185 185
186 186
      conn = delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug")
187 187
188 188
      assert response(conn, 204) == ""
189
      assert Labels.list_labels() == []
189
      assert Labels.list_labels(repository()) == []
190 190
    end
191 191
192 192
    test "DELETE /api/v3/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do

@@ -195,4 +195,8 @@ defmodule OpenAgentsWeb.LabelControllerTest do

195 195
      assert json_response(conn, 404) == %{"message" => "Not Found"}
196 196
    end
197 197
  end
198
199
  defp repository do
200
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
201
  end
198 202
end
test/openagents_web/controllers/milestone_controller_test.exs modified +27 -15

@@ -1,7 +1,7 @@

1 1
defmodule OpenAgentsWeb.MilestoneControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "milestones")}
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "milestones", repository())}
5 5
6 6
  import OpenAgents.MilestonesFixtures
7 7

@@ -10,7 +10,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

10 10
11 11
  describe "index" do
12 12
    test "GET /api/v3/repos/:owner/:repo/milestones lists milestones", %{conn: conn} do
13
      milestone_fixture(%{title: "v1.0", state: "open"})
13
      milestone_fixture(repository(), %{title: "v1.0", state: "open"})
14 14
15 15
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones")
16 16

@@ -20,11 +20,17 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

20 20
    end
21 21
22 22
    test "GET /api/v3/repos/:owner/:repo/milestones renders issue counts and url", %{conn: conn} do
23
      milestone = milestone_fixture(%{title: "v1.0"})
24
      {:ok, _open_issue} = Issues.create_issue(%{title: "Open", milestone: milestone.number})
23
      milestone = milestone_fixture(repository(), %{title: "v1.0"})
24
25
      {:ok, _open_issue} =
26
        Issues.create_issue(repository(), %{title: "Open", milestone: milestone.number})
25 27
26 28
      {:ok, _closed_issue} =
27
        Issues.create_issue(%{title: "Closed", milestone: milestone.number, state: "closed"})
29
        Issues.create_issue(repository(), %{
30
          title: "Closed",
31
          milestone: milestone.number,
32
          state: "closed"
33
        })
28 34
29 35
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones")
30 36

@@ -58,7 +64,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

58 64
               "number" => number
59 65
             } = json_response(conn, 201)
60 66
61
      assert Milestones.get_milestone_by_number!(number).title == "v2.0"
67
      assert Milestones.get_milestone_by_number!(repository(), number).title == "v2.0"
62 68
    end
63 69
64 70
    test "POST /api/v3/repos/:owner/:repo/milestones assigns sequential numbers", %{conn: conn} do

@@ -78,13 +84,13 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

78 84
        })
79 85
80 86
      assert %{"errors" => %{"title" => _}} = json_response(conn, 422)
81
      assert Milestones.list_milestones() == []
87
      assert Milestones.list_milestones(repository()) == []
82 88
    end
83 89
  end
84 90
85 91
  describe "show" do
86 92
    test "GET /api/v3/repos/:owner/:repo/milestones/:milestone_number returns it", %{conn: conn} do
87
      milestone = milestone_fixture(%{title: "Show me"})
93
      milestone = milestone_fixture(repository(), %{title: "Show me"})
88 94
89 95
      conn =
90 96
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}")

@@ -103,7 +109,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

103 109
104 110
  describe "update" do
105 111
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number closes it", %{conn: conn} do
106
      milestone = milestone_fixture(%{title: "Close me", state: "open"})
112
      milestone = milestone_fixture(repository(), %{title: "Close me", state: "open"})
107 113
108 114
      conn =
109 115
        patch(

@@ -115,13 +121,13 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

115 121
        )
116 122
117 123
      assert json_response(conn, 200)["state"] == "closed"
118
      assert Milestones.get_milestone_by_number!(milestone.number).state == "closed"
124
      assert Milestones.get_milestone_by_number!(repository(), milestone.number).state == "closed"
119 125
    end
120 126
121 127
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number retitles it", %{
122 128
      conn: conn
123 129
    } do
124
      milestone = milestone_fixture(%{title: "Old title"})
130
      milestone = milestone_fixture(repository(), %{title: "Old title"})
125 131
126 132
      conn =
127 133
        patch(

@@ -137,7 +143,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

137 143
138 144
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 422 for a blank title",
139 145
         %{conn: conn} do
140
      milestone = milestone_fixture(%{title: "Keep me"})
146
      milestone = milestone_fixture(repository(), %{title: "Keep me"})
141 147
142 148
      conn =
143 149
        conn

@@ -148,7 +154,9 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

148 154
        )
149 155
150 156
      assert %{"errors" => %{"title" => _}} = json_response(conn, 422)
151
      assert Milestones.get_milestone_by_number!(milestone.number).title == "Keep me"
157
158
      assert Milestones.get_milestone_by_number!(repository(), milestone.number).title ==
159
               "Keep me"
152 160
    end
153 161
154 162
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",

@@ -166,7 +174,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

166 174
    test "DELETE /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 204", %{
167 175
      conn: conn
168 176
    } do
169
      milestone = milestone_fixture(%{title: "Delete me"})
177
      milestone = milestone_fixture(repository(), %{title: "Delete me"})
170 178
171 179
      conn =
172 180
        delete(

@@ -175,7 +183,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

175 183
        )
176 184
177 185
      assert response(conn, 204) == ""
178
      assert Milestones.list_milestones() == []
186
      assert Milestones.list_milestones(repository()) == []
179 187
    end
180 188
181 189
    test "DELETE /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",

@@ -185,4 +193,8 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

185 193
      assert json_response(conn, 404) == %{"message" => "Not Found"}
186 194
    end
187 195
  end
196
197
  defp repository do
198
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
199
  end
188 200
end
test/openagents_web/controllers/og_image_controller_test.exs modified +10 -4

@@ -58,7 +58,7 @@ defmodule OpenAgentsWeb.OgImageControllerTest do

58 58
    end)
59 59
60 60
    shas = seed_repo("openagents.com")
61
    repository = Repositories.initial_repository!()
61
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
62 62
63 63
    {:ok, repository: repository, shas: shas}
64 64
  end

@@ -91,7 +91,7 @@ defmodule OpenAgentsWeb.OgImageControllerTest do

91 91
  end
92 92
93 93
  test "issue cards render from the public issue path", %{conn: conn} do
94
    {:ok, issue} = Issues.create_issue(%{"title" => "Carded issue"})
94
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Carded issue"})
95 95
    card = OpenAgentsWeb.OG.issue("OpenAgentsInc", "openagents.com", issue)
96 96
97 97
    assert response(get(conn, signed_url(card)), 200) == @marker_png

@@ -155,7 +155,9 @@ defmodule OpenAgentsWeb.OgImageControllerTest do

155 155
156 156
    real_path =
157 157
      OpenAgentsWeb.OG.request_path(
158
        OpenAgentsWeb.OG.repo_card_for(Repositories.initial_repository!())
158
        OpenAgentsWeb.OG.repo_card_for(
159
          Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
160
        )
159 161
      )
160 162
161 163
    bad_signature = real_path <> "?sig=bogus"

@@ -228,7 +230,7 @@ defmodule OpenAgentsWeb.OgImageControllerTest do

228 230
  end
229 231
230 232
  test "an issue page emits an issue-specific card URL", %{conn: conn} do
231
    {:ok, issue} = Issues.create_issue(%{"title" => "Shared on social"})
233
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Shared on social"})
232 234
233 235
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}")
234 236

@@ -328,4 +330,8 @@ defmodule OpenAgentsWeb.OgImageControllerTest do

328 330
      File.rm(input)
329 331
    end
330 332
  end
333
334
  defp repository do
335
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
336
  end
331 337
end
test/openagents_web/controllers/project_controller_test.exs modified +225 -102

@@ -1,20 +1,36 @@

1 1
defmodule OpenAgentsWeb.ProjectControllerTest do
2 2
  use OpenAgentsWeb.ConnCase
3 3
4
  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "projects", "alice")}
5
6
  import OpenAgents.ProjectFieldsFixtures
7
  import OpenAgents.ProjectItemsFixtures
8
  import OpenAgents.ProjectsFixtures
9
10 4
  alias OpenAgents.Issues
11 5
  alias OpenAgents.Projects
12 6
7
  @owner "ProjectTestOrg"
8
  @repo "project-api"
9
10
  setup %{conn: conn} do
11
    user = github_user("api-token-projects", "alice")
12
13
    repository =
14
      repository_with_member_fixture(user, %{
15
        owner: @owner,
16
        name: @repo,
17
        visibility: "private"
18
      })
19
20
    Process.put({__MODULE__, :repository}, repository)
21
    on_exit(fn -> Process.delete({__MODULE__, :repository}) end)
22
23
    {:ok,
24
     conn: put_forge_api_token(conn, "projects", "alice"), repository: repository, user: user}
25
  end
26
13 27
  describe "index" do
14
    test "GET /api/v3/users/:username/projectsV2 lists that user's projects", %{conn: conn} do
28
    test "GET /api/v3/repos/:owner/:repo/projectsV2 lists the repository's projects", %{
29
      conn: conn
30
    } do
15 31
      project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
16 32
17
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2")
33
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
18 34
19 35
      assert %{"projects" => [project]} = json_response(conn, 200)
20 36
      assert project["title"] == "Roadmap"

@@ -22,64 +38,76 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

22 38
      assert project["state"] == "open"
23 39
    end
24 40
25
    test "GET /api/v3/users/:username/projectsV2 excludes other owners' projects", %{conn: conn} do
41
    test "GET /api/v3/repos/:owner/:repo/projectsV2 excludes another repository", %{conn: conn} do
26 42
      project_fixture(%{title: "Mine", owner: "alice"})
27
      project_fixture(%{title: "Theirs", owner: "bob"})
43
      other_repository = repository_fixture(%{owner: "Elsewhere", name: "other-projects"})
28 44
29
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2")
45
      {:ok, _project} =
46
        Projects.create_project(other_repository, %{title: "Theirs", owner: "bob"})
47
48
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
30 49
31 50
      assert %{"projects" => [%{"title" => "Mine"}]} = json_response(conn, 200)
32 51
    end
33 52
34
    test "GET /api/v3/users/:username/projectsV2 returns an empty list for an unknown user", %{
35
      conn: conn
36
    } do
37
      conn = get(conn, ~p"/api/v3/users/nobody/projectsV2")
38
39
      assert json_response(conn, 200) == %{"projects" => []}
53
    test "GET /api/v3/repos/:owner/:repo/projectsV2 hides a private repository from a non-member" do
54
      conn = get(build_conn(), ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
55
      assert json_response(conn, 404) == %{"message" => "Not Found"}
40 56
    end
41 57
  end
42 58
43 59
  describe "create" do
44
    test "POST /api/v3/:owner/projectsV2 creates a project", %{conn: conn} do
45
      conn = post(conn, ~p"/api/v3/alice/projectsV2", %{title: "New board"})
60
    test "POST /api/v3/repos/:owner/:repo/projectsV2 creates a project", %{conn: conn} do
61
      conn =
62
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{
63
          title: "New board"
64
        })
46 65
47 66
      assert %{"title" => "New board", "owner" => "alice", "number" => number} =
48 67
               json_response(conn, 201)
49 68
50
      assert Projects.get_project_by_number!(number).title == "New board"
69
      assert Projects.get_project_by_number!(repository(), number).title == "New board"
51 70
    end
52 71
53
    test "POST /api/v3/:owner/projectsV2 takes the owner from the path", %{conn: conn} do
54
      conn = post(conn, ~p"/api/v3/alice/projectsV2", %{title: "Board", owner: "mallory"})
72
    test "POST /api/v3/repos/:owner/:repo/projectsV2 ignores repository override params", %{
73
      conn: conn
74
    } do
75
      conn =
76
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{
77
          title: "Board",
78
          owner: "mallory",
79
          repo: "elsewhere"
80
        })
55 81
56 82
      assert json_response(conn, 201)["owner"] == "alice"
57 83
    end
58 84
59
    test "POST /api/v3/:owner/projectsV2 returns 422 without a title", %{conn: conn} do
60
      conn = post(conn, ~p"/api/v3/alice/projectsV2", %{})
85
    test "POST /api/v3/repos/:owner/:repo/projectsV2 returns 422 without a title", %{
86
      conn: conn
87
    } do
88
      conn = post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{})
61 89
62 90
      assert %{"errors" => %{"title" => _}} = json_response(conn, 422)
63
      assert Projects.list_projects() == []
91
      assert Projects.list_projects(repository()) == []
64 92
    end
65 93
  end
66 94
67 95
  describe "show" do
68
    test "GET /api/v3/users/:username/projectsV2/:project_number returns the project", %{
96
    test "GET /api/v3/repos/:owner/:repo/projectsV2/:project_number returns the project", %{
69 97
      conn: conn
70 98
    } do
71 99
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
72 100
73
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}")
101
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}")
74 102
75 103
      assert %{"title" => "Roadmap", "id" => id} = json_response(conn, 200)
76 104
      assert id == project.id
77 105
    end
78 106
79
    test "GET /api/v3/users/:username/projectsV2/:project_number returns 404 when missing", %{
107
    test "GET /api/v3/repos/:owner/:repo/projectsV2/:project_number returns 404 when missing", %{
80 108
      conn: conn
81 109
    } do
82
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/999999")
110
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999")
83 111
84 112
      assert json_response(conn, 404) == %{"message" => "Not Found"}
85 113
    end

@@ -91,7 +119,8 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

91 119
    } do
92 120
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
93 121
94
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items")
122
      conn =
123
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
95 124
96 125
      assert json_response(conn, 200) == %{"items" => []}
97 126
    end

@@ -100,7 +129,8 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

100 129
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
101 130
      item = project_item_fixture(%{project_id: project.id, values: %{"Status" => "Todo"}})
102 131
103
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items")
132
      conn =
133
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
104 134
105 135
      assert %{"items" => [rendered]} = json_response(conn, 200)
106 136
      assert rendered["id"] == item.id

@@ -111,7 +141,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

111 141
    test "GET .../projectsV2/:project_number/items returns 404 for a missing project", %{
112 142
      conn: conn
113 143
    } do
114
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/999999/items")
144
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/items")
115 145
116 146
      assert json_response(conn, 404) == %{"message" => "Not Found"}
117 147
    end

@@ -120,7 +150,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

120 150
  describe "create_item" do
121 151
    setup do
122 152
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
123
      {:ok, issue} = Issues.create_issue(%{title: "Trackable issue"})
153
      {:ok, issue} = create_issue(%{title: "Trackable issue"})
124 154
      %{project: project, issue: issue}
125 155
    end
126 156

@@ -130,14 +160,18 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

130 160
      issue: issue
131 161
    } do
132 162
      conn =
133
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{
134
          issue_number: issue.number
135
        })
163
        post(
164
          conn,
165
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
166
          %{
167
            issue_number: issue.number
168
          }
169
        )
136 170
137 171
      assert %{"items" => [item]} = json_response(conn, 201)
138 172
      assert item["issue_id"] == issue.id
139 173
140
      assert [%{issue_id: issue_id}] = Projects.list_project_items(project.id)
174
      assert [%{issue_id: issue_id}] = Projects.list_project_items(project)
141 175
      assert issue_id == issue.id
142 176
    end
143 177

@@ -147,10 +181,14 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

147 181
      issue: issue
148 182
    } do
149 183
      conn =
150
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{
151
          issue_number: issue.number,
152
          values: %{"Status" => "In Progress"}
153
        })
184
        post(
185
          conn,
186
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
187
          %{
188
            issue_number: issue.number,
189
            values: %{"Status" => "In Progress"}
190
          }
191
        )
154 192
155 193
      assert %{"items" => [%{"values" => %{"Status" => "In Progress"}}]} =
156 194
               json_response(conn, 201)

@@ -162,34 +200,47 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

162 200
      issue: issue
163 201
    } do
164 202
      conn =
165
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{
166
          issue_number: issue.number,
167
          values: "not-a-map"
168
        })
203
        post(
204
          conn,
205
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
206
          %{
207
            issue_number: issue.number,
208
            values: "not-a-map"
209
          }
210
        )
169 211
170 212
      assert %{"errors" => %{"values" => _}} = json_response(conn, 422)
171
      assert Projects.list_project_items(project.id) == []
213
      assert Projects.list_project_items(project) == []
172 214
    end
173 215
174 216
    test "POST .../projectsV2/:project_number/items returns 422 without an issue_number", %{
175 217
      conn: conn,
176 218
      project: project
177 219
    } do
178
      conn = post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{})
220
      conn =
221
        post(
222
          conn,
223
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
224
          %{}
225
        )
179 226
180 227
      assert %{"errors" => %{"issue_number" => _}} = json_response(conn, 422)
181
      assert Projects.list_project_items(project.id) == []
228
      assert Projects.list_project_items(project) == []
182 229
    end
183 230
184 231
    test "POST .../projectsV2/:project_number/items returns 422 for a non-numeric issue_number",
185 232
         %{conn: conn, project: project} do
186 233
      conn =
187
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{
188
          issue_number: "not-a-number"
189
        })
234
        post(
235
          conn,
236
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
237
          %{
238
            issue_number: "not-a-number"
239
          }
240
        )
190 241
191 242
      assert %{"errors" => %{"issue_number" => _}} = json_response(conn, 422)
192
      assert Projects.list_project_items(project.id) == []
243
      assert Projects.list_project_items(project) == []
193 244
    end
194 245
195 246
    test "POST .../projectsV2/:project_number/items returns 404 for an unknown issue", %{

@@ -197,9 +248,13 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

197 248
      project: project
198 249
    } do
199 250
      conn =
200
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items", %{
201
          issue_number: 999_999
202
        })
251
        post(
252
          conn,
253
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
254
          %{
255
            issue_number: 999_999
256
          }
257
        )
203 258
204 259
      assert json_response(conn, 404) == %{"message" => "Not Found"}
205 260
    end

@@ -209,7 +264,9 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

209 264
      issue: issue
210 265
    } do
211 266
      conn =
212
        post(conn, ~p"/api/v3/users/alice/projectsV2/999999/items", %{issue_number: issue.number})
267
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/items", %{
268
          issue_number: issue.number
269
        })
213 270
214 271
      assert json_response(conn, 404) == %{"message" => "Not Found"}
215 272
    end

@@ -228,12 +285,16 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

228 285
      item: item
229 286
    } do
230 287
      conn =
231
        patch(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items/#{item.id}", %{
232
          values: %{"Status" => "Done"}
233
        })
288
        patch(
289
          conn,
290
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
291
          %{
292
            values: %{"Status" => "Done"}
293
          }
294
        )
234 295
235 296
      assert %{"items" => [%{"values" => %{"Status" => "Done"}}]} = json_response(conn, 200)
236
      assert Projects.get_project_item!(item.id).values == %{"Status" => "Done"}
297
      assert Projects.get_project_item!(project, item.id).values == %{"Status" => "Done"}
237 298
    end
238 299
239 300
    test "PATCH .../items/:item_id keeps values it was not asked to change", %{

@@ -242,9 +303,13 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

242 303
      item: item
243 304
    } do
244 305
      conn =
245
        patch(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items/#{item.id}", %{
246
          values: %{"Priority" => "P1"}
247
        })
306
        patch(
307
          conn,
308
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
309
          %{
310
            values: %{"Priority" => "P1"}
311
          }
312
        )
248 313
249 314
      assert %{"items" => [%{"values" => values}]} = json_response(conn, 200)
250 315
      assert values == %{"Status" => "Todo", "Priority" => "P1"}

@@ -256,12 +321,16 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

256 321
      item: item
257 322
    } do
258 323
      conn =
259
        patch(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items/#{item.id}", %{
260
          values: "not-a-map"
261
        })
324
        patch(
325
          conn,
326
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
327
          %{
328
            values: "not-a-map"
329
          }
330
        )
262 331
263 332
      assert %{"errors" => %{"values" => _}} = json_response(conn, 422)
264
      assert Projects.get_project_item!(item.id).values == %{"Status" => "Todo"}
333
      assert Projects.get_project_item!(project, item.id).values == %{"Status" => "Todo"}
265 334
    end
266 335
267 336
    test "PATCH .../items/:item_id returns 404 for a missing item", %{

@@ -269,9 +338,13 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

269 338
      project: project
270 339
    } do
271 340
      conn =
272
        patch(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/items/999999", %{
273
          values: %{"Status" => "Done"}
274
        })
341
        patch(
342
          conn,
343
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/999999",
344
          %{
345
            values: %{"Status" => "Done"}
346
          }
347
        )
275 348
276 349
      assert json_response(conn, 404) == %{"message" => "Not Found"}
277 350
    end

@@ -283,7 +356,11 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

283 356
    } do
284 357
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
285 358
286
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/fields")
359
      conn =
360
        get(
361
          conn,
362
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
363
        )
287 364
288 365
      assert json_response(conn, 200) == %{"fields" => []}
289 366
    end

@@ -299,7 +376,11 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

299 376
          options: %{"values" => ["Todo", "Done"]}
300 377
        })
301 378
302
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/fields")
379
      conn =
380
        get(
381
          conn,
382
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
383
        )
303 384
304 385
      assert %{"fields" => [rendered]} = json_response(conn, 200)
305 386
      assert rendered["id"] == field.id

@@ -311,7 +392,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

311 392
    test "GET .../projectsV2/:project_number/fields returns 404 for a missing project", %{
312 393
      conn: conn
313 394
    } do
314
      conn = get(conn, ~p"/api/v3/users/alice/projectsV2/999999/fields")
395
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/fields")
315 396
316 397
      assert json_response(conn, 404) == %{"message" => "Not Found"}
317 398
    end

@@ -320,11 +401,15 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

320 401
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
321 402
322 403
      conn =
323
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/fields", %{
324
          name: "Status",
325
          data_type: "single_select",
326
          options: %{values: ["To Do", "In Progress", "Done"]}
327
        })
404
        post(
405
          conn,
406
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
407
          %{
408
            name: "Status",
409
            data_type: "single_select",
410
            options: %{values: ["To Do", "In Progress", "Done"]}
411
          }
412
        )
328 413
329 414
      assert %{
330 415
               "fields" => [

@@ -345,7 +430,12 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

345 430
    } do
346 431
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
347 432
348
      conn = post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/fields", %{})
433
      conn =
434
        post(
435
          conn,
436
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
437
          %{}
438
        )
349 439
350 440
      assert %{"errors" => errors} = json_response(conn, 422)
351 441
      assert Map.has_key?(errors, "name")

@@ -360,11 +450,15 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

360 450
      other_project = project_fixture(%{title: "Other", owner: "alice"})
361 451
362 452
      conn =
363
        post(conn, ~p"/api/v3/users/alice/projectsV2/#{project.number}/fields", %{
364
          name: "Status",
365
          data_type: "single_select",
366
          project_id: other_project.id
367
        })
453
        post(
454
          conn,
455
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
456
          %{
457
            name: "Status",
458
            data_type: "single_select",
459
            project_id: other_project.id
460
          }
461
        )
368 462
369 463
      assert json_response(conn, 201)
370 464
      assert [%{project_id: project_id}] = Projects.list_project_fields(project)

@@ -373,12 +467,11 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

373 467
    end
374 468
  end
375 469
376
  describe "owner-path authorization" do
377
    test "another username cannot read or mutate a project through any project action", %{
378
      conn: conn
379
    } do
470
  describe "repository authorization" do
471
    test "a non-member cannot read or mutate a private repository's project" do
380 472
      project = project_fixture(%{title: "Alice only", owner: "alice"})
381
      {:ok, issue} = Issues.create_issue(%{title: "Tracked"})
473
      {:ok, issue} = create_issue(%{title: "Tracked"})
474
      mallory_conn = put_forge_api_token(build_conn(), "project-mallory", "mallory")
382 475
383 476
      {:ok, item} =
384 477
        Projects.create_project_item(%{"issue_number" => issue.number}, project)

@@ -390,44 +483,74 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

390 483
          data_type: "single_select"
391 484
        })
392 485
393
      assert get(conn, ~p"/api/v3/users/bob/projectsV2/#{project.number}")
486
      assert get(
487
               mallory_conn,
488
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
489
             )
394 490
             |> json_response(404) == %{"message" => "Not Found"}
395 491
396
      assert get(recycle(conn), ~p"/api/v3/users/bob/projectsV2/#{project.number}/items")
492
      assert get(
493
               recycle(mallory_conn),
494
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
495
             )
397 496
             |> json_response(404) == %{"message" => "Not Found"}
398 497
399
      assert get(recycle(conn), ~p"/api/v3/users/bob/projectsV2/#{project.number}/fields")
498
      assert get(
499
               recycle(mallory_conn),
500
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
501
             )
400 502
             |> json_response(404) == %{"message" => "Not Found"}
401 503
402 504
      assert post(
403
               recycle(conn),
404
               ~p"/api/v3/users/bob/projectsV2/#{project.number}/items",
505
               recycle(mallory_conn),
506
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
405 507
               %{issue_number: issue.number}
406 508
             )
407 509
             |> json_response(404) == %{"message" => "Not Found"}
408 510
409 511
      assert patch(
410
               recycle(conn),
411
               ~p"/api/v3/users/bob/projectsV2/#{project.number}/items/#{item.id}",
512
               recycle(mallory_conn),
513
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
412 514
               %{values: %{"Status" => "Done"}}
413 515
             )
414 516
             |> json_response(404) == %{"message" => "Not Found"}
415 517
416 518
      assert post(
417
               recycle(conn),
418
               ~p"/api/v3/users/bob/projectsV2/#{project.number}/fields",
519
               recycle(mallory_conn),
520
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
419 521
               %{name: "Priority", data_type: "single_select"}
420 522
             )
421 523
             |> json_response(404) == %{"message" => "Not Found"}
422 524
423
      assert Projects.get_project_item!(item.id).values == %{}
525
      assert Projects.get_project_item!(project, item.id).values == %{}
424 526
    end
425 527
426
    test "a token owner cannot create a project for another username", %{conn: conn} do
427
      assert post(conn, ~p"/api/v3/bob/projectsV2", %{title: "Not Bob's"})
528
    test "a token owner cannot create a project in another repository", %{conn: conn} do
529
      other_repository =
530
        repository_fixture(%{owner: "OtherOrg", name: "other-private", visibility: "private"})
531
532
      assert post(conn, ~p"/api/v3/repos/OtherOrg/other-private/projectsV2", %{
533
               title: "Not Alice's"
534
             })
428 535
             |> json_response(404) == %{"message" => "Not Found"}
429 536
430
      assert Projects.list_projects() == []
537
      assert Projects.list_projects(other_repository) == []
431 538
    end
432 539
  end
540
541
  defp repository, do: Process.get({__MODULE__, :repository})
542
543
  defp project_fixture(attrs) do
544
    OpenAgents.ProjectsFixtures.project_fixture(repository(), attrs)
545
  end
546
547
  defp project_item_fixture(attrs) do
548
    OpenAgents.ProjectItemsFixtures.project_item_fixture(repository(), attrs)
549
  end
550
551
  defp project_field_fixture(attrs) do
552
    OpenAgents.ProjectFieldsFixtures.project_field_fixture(repository(), attrs)
553
  end
554
555
  defp create_issue(attrs), do: Issues.create_issue(repository(), attrs)
433 556
end
test/openagents_web/controllers/repository_isolation_controller_test.exs modified +1 -1

@@ -8,7 +8,7 @@ defmodule OpenAgentsWeb.RepositoryIsolationControllerTest do

8 8
9 9
  setup %{conn: conn} do
10 10
    conn = put_forge_api_token(conn, "repository-isolation")
11
    initial = Repositories.initial_repository!()
11
    initial = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
12 12
13 13
    {:ok, second} =
14 14
      Repositories.create_repository(%{
test/openagents_web/home_controller_test.exs modified +1 -1

@@ -38,7 +38,7 @@ defmodule OpenAgentsWeb.HomeControllerTest do

38 38
39 39
  test "an authenticated account sees the dashboard before the first import", %{conn: conn} do
40 40
    conn = log_in_github_user(conn, "authenticated-empty-home-user")
41
    Repositories.initial_repository!() |> Repo.delete!()
41
    Repositories.get_by_path!("OpenAgentsInc", "openagents.com") |> Repo.delete!()
42 42
43 43
    html = html_response(get(conn, ~p"/"), 200)
44 44
test/openagents_web/live/assignee_index_live_test.exs modified +12 -6

@@ -7,7 +7,7 @@ defmodule OpenAgentsWeb.AssigneeIndexLiveTest do

7 7
  import OpenAgents.AccountsFixtures
8 8
9 9
  setup %{conn: conn} do
10
    {:ok, conn: log_in_github_user(conn, "assignee-index")}
10
    {:ok, conn: log_in_repository_user(conn, "assignee-index", repository())}
11 11
  end
12 12
13 13
  test "mounts with an honest empty state when no issue has an assignee", %{conn: conn} do

@@ -19,20 +19,22 @@ defmodule OpenAgentsWeb.AssigneeIndexLiveTest do

19 19
  end
20 20
21 21
  test "tallies assignees across open and closed issues, most-assigned first", %{conn: conn} do
22
    repository_user_fixture("ada-assignee")
23
    repository_user_fixture("grace-assignee")
22
    for login <- ["ada-assignee", "grace-assignee"] do
23
      user = repository_user_fixture(login)
24
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
25
    end
24 26
25 27
    {:ok, _first} =
26
      Issues.create_issue(%{
28
      Issues.create_issue(repository(), %{
27 29
        "title" => "First",
28 30
        "assignees" => ["ada-assignee", "grace-assignee"]
29 31
      })
30 32
31 33
    {:ok, _second} =
32
      Issues.create_issue(%{"title" => "Second", "assignees" => ["ada-assignee"]})
34
      Issues.create_issue(repository(), %{"title" => "Second", "assignees" => ["ada-assignee"]})
33 35
34 36
    {:ok, third} =
35
      Issues.create_issue(%{"title" => "Third", "assignees" => ["ada-assignee"]})
37
      Issues.create_issue(repository(), %{"title" => "Third", "assignees" => ["ada-assignee"]})
36 38
37 39
    # A closed issue still counts: the view lists `state: "all"`.
38 40
    {:ok, _} = Issues.update_issue(third, %{"state" => "closed"})

@@ -56,4 +58,8 @@ defmodule OpenAgentsWeb.AssigneeIndexLiveTest do

56 58
57 59
    refute to == "/OpenAgentsInc/openagents.com/assignees"
58 60
  end
61
62
  defp repository do
63
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
64
  end
59 65
end
test/openagents_web/live/issue_access_live_test.exs modified +16 -9

@@ -33,12 +33,15 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

33 33
    {path, _flash} = assert_redirect(view)
34 34
    number = path |> String.split("/") |> List.last() |> String.to_integer()
35 35
36
    Issues.get_issue_by_number!(Repositories.initial_repository!(), number)
36
    Issues.get_issue_by_number!(
37
      Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
38
      number
39
    )
37 40
  end
38 41
39 42
  describe "anonymous visitors" do
40 43
    test "read a public issue and its timeline without any write controls" do
41
      {:ok, issue} = Issues.create_issue(%{"title" => "Readable by anyone"})
44
      {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Readable by anyone"})
42 45
      {:ok, _} = Issues.create_comment(issue, %{body: "First comment"}, nil)
43 46
44 47
      {:ok, view, html} =

@@ -90,7 +93,7 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

90 93
    end
91 94
92 95
    test "can comment on an open conversation", %{conn: conn} do
93
      {:ok, issue} = Issues.create_issue(%{"title" => "Needs a reply"})
96
      {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Needs a reply"})
94 97
      {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}")
95 98
96 99
      view

@@ -104,7 +107,7 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

104 107
    end
105 108
106 109
    test "cannot post to a conversation locked after the page opened", %{conn: conn} do
107
      {:ok, issue} = Issues.create_issue(%{"title" => "Lock this thread"})
110
      {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Lock this thread"})
108 111
      {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}")
109 112
      {:ok, _locked} = Issues.update_issue(issue, %{"locked" => true})
110 113

@@ -115,18 +118,18 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

115 118
    end
116 119
117 120
    test "get no triage controls anywhere on the issue page", %{conn: conn} do
118
      {:ok, issue} = Issues.create_issue(%{"title" => "Not yours to close"})
121
      {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Not yours to close"})
119 122
      {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}")
120 123
121 124
      refute has_element?(view, ~s{button[phx-click="close"]})
122 125
      refute has_element?(view, ~s{button[phx-click="toggle_edit"]})
123 126
      refute has_element?(view, ~s{[id^="row-state-"]})
124 127
125
      assert Issues.get_issue!(issue.id).state == "open"
128
      assert Issues.get_issue!(repository(), issue.id).state == "open"
126 129
    end
127 130
128 131
    test "see the issue list with a filing link but no row menus", %{conn: conn} do
129
      {:ok, _issue} = Issues.create_issue(%{"title" => "Visible to all"})
132
      {:ok, _issue} = Issues.create_issue(repository(), %{"title" => "Visible to all"})
130 133
131 134
      {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
132 135

@@ -170,7 +173,7 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

170 173
      |> form("#issue-edit-form", issue: %{title: "Fixed title", body: "Now with steps"})
171 174
      |> render_submit()
172 175
173
      updated = Issues.get_issue!(issue.id)
176
      updated = Issues.get_issue!(repository(), issue.id)
174 177
      assert updated.title == "Fixed title"
175 178
      assert updated.body == "Now with steps"
176 179
    end

@@ -185,9 +188,13 @@ defmodule OpenAgentsWeb.IssueAccessLiveTest do

185 188
      |> element(~s{button[phx-click="close"]})
186 189
      |> render_click()
187 190
188
      closed = Issues.get_issue!(issue.id)
191
      closed = Issues.get_issue!(repository(), issue.id)
189 192
      assert closed.state == "closed"
190 193
      assert closed.state_reason == "completed"
191 194
    end
192 195
  end
196
197
  defp repository do
198
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
199
  end
193 200
end
test/openagents_web/live/issue_index_live_test.exs modified +28 -19

@@ -8,7 +8,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

8 8
  alias OpenAgents.Issues
9 9
10 10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "issue-index")}
11
    {:ok, conn: log_in_repository_user(conn, "issue-index", repository())}
12 12
  end
13 13
14 14
  test "mounts with zeroed counts and an empty state", %{conn: conn} do

@@ -33,11 +33,12 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

33 33
  end
34 34
35 35
  test "lists open issues with their number, author, labels, and assignees", %{conn: conn} do
36
    repository_user_fixture("grace-index")
37
    label_fixture(%{name: "bug", color: "d73a4a"})
36
    grace = repository_user_fixture("grace-index")
37
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), grace, "contributor")
38
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
38 39
39 40
    {:ok, issue} =
40
      Issues.create_issue(%{
41
      Issues.create_issue(repository(), %{
41 42
        "title" => "Streaming stalls",
42 43
        "user" => %{"login" => "ada"},
43 44
        "labels" => ["bug"],

@@ -62,7 +63,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

62 63
  end
63 64
64 65
  test "an issue with no author falls back to anonymous", %{conn: conn} do
65
    {:ok, _} = Issues.create_issue(%{"title" => "Orphaned"})
66
    {:ok, _} = Issues.create_issue(repository(), %{"title" => "Orphaned"})
66 67
67 68
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
68 69

@@ -71,7 +72,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

71 72
  end
72 73
73 74
  test "the comment count only renders once an issue has comments", %{conn: conn} do
74
    {:ok, issue} = Issues.create_issue(%{"title" => "Chatty"})
75
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Chatty"})
75 76
76 77
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
77 78
    refute has_element?(view, ~s{svg[data-icon="comment"]})

@@ -88,8 +89,8 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

88 89
  end
89 90
90 91
  test "patching to the closed filter swaps the stream and the current marker", %{conn: conn} do
91
    {:ok, _open} = Issues.create_issue(%{"title" => "Open one"})
92
    {:ok, closed} = Issues.create_issue(%{"title" => "Closed one"})
92
    {:ok, _open} = Issues.create_issue(repository(), %{"title" => "Open one"})
93
    {:ok, closed} = Issues.create_issue(repository(), %{"title" => "Closed one"})
93 94
    {:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
94 95
95 96
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")

@@ -116,7 +117,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

116 117
  end
117 118
118 119
  test "the closed filter has its own empty-state wording", %{conn: conn} do
119
    {:ok, _open} = Issues.create_issue(%{"title" => "Open one"})
120
    {:ok, _open} = Issues.create_issue(repository(), %{"title" => "Open one"})
120 121
121 122
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues?state=closed")
122 123

@@ -125,9 +126,9 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

125 126
  end
126 127
127 128
  test "the open and closed counts stay visible on both filters", %{conn: conn} do
128
    {:ok, _} = Issues.create_issue(%{"title" => "A"})
129
    {:ok, _} = Issues.create_issue(%{"title" => "B"})
130
    {:ok, c} = Issues.create_issue(%{"title" => "C"})
129
    {:ok, _} = Issues.create_issue(repository(), %{"title" => "A"})
130
    {:ok, _} = Issues.create_issue(repository(), %{"title" => "B"})
131
    {:ok, c} = Issues.create_issue(repository(), %{"title" => "C"})
131 132
    {:ok, _} = Issues.update_issue(c, %{"state" => "closed"})
132 133
133 134
    for state <- ["open", "closed"] do

@@ -152,7 +153,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

152 153
  # and both are GitHub fields. Labels and milestone need option lists longer
153 154
  # than a row can explain, so they are edited from the issue page's rail.
154 155
  test "closing an issue from its row drops it out of the open filter", %{conn: conn} do
155
    {:ok, issue} = Issues.create_issue(%{"title" => "Closeable"})
156
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Closeable"})
156 157
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
157 158
158 159
    assert has_element?(view, ~s{#row-state-#{issue.id}})

@@ -161,7 +162,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

161 162
    |> element(~s{#row-state-#{issue.id} button}, "Closed as not planned")
162 163
    |> render_click()
163 164
164
    closed = Issues.get_issue!(issue.id)
165
    closed = Issues.get_issue!(repository(), issue.id)
165 166
    assert closed.state == "closed"
166 167
    assert closed.state_reason == "not_planned"
167 168

@@ -177,22 +178,26 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

177 178
  end
178 179
179 180
  test "assigning from a row keeps the row and updates the face", %{conn: conn} do
180
    repository_user_fixture("hopper-index")
181
    {:ok, issue} = Issues.create_issue(%{"title" => "Assignable"})
181
    hopper = repository_user_fixture("hopper-index")
182
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), hopper, "contributor")
183
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Assignable"})
182 184
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
183 185
184 186
    view
185 187
    |> element(~s{#row-assignee-#{issue.id} button}, "hopper-index")
186 188
    |> render_click()
187 189
188
    assert Issues.get_issue!(issue.id).assignees |> Enum.map(& &1["login"]) == ["hopper-index"]
190
    assert Issues.get_issue!(repository(), issue.id).assignees |> Enum.map(& &1["login"]) == [
191
             "hopper-index"
192
           ]
193
189 194
    assert has_element?(view, ~s{#row-assignee-#{issue.id}})
190 195
    assert has_element?(view, ~s{[title="hopper-index"]})
191 196
  end
192 197
193 198
  test "the row's controls sit outside the link to the issue", %{conn: conn} do
194 199
    # A state-changing control inside a link target is how people mis-click.
195
    {:ok, issue} = Issues.create_issue(%{"title" => "Separate hit areas"})
200
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Separate hit areas"})
196 201
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues")
197 202
198 203
    title_link =

@@ -211,7 +216,7 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

211 216
  # way reading its code is: no redirect, no controls, and an invitation to
212 217
  # sign in rather than a wall.
213 218
  test "an anonymous visitor reads a public repository's issue list" do
214
    {:ok, _issue} = Issues.create_issue(%{"title" => "Public spectable"})
219
    {:ok, _issue} = Issues.create_issue(repository(), %{"title" => "Public spectable"})
215 220
216 221
    {:ok, view, html} = live(build_conn(), ~p"/OpenAgentsInc/openagents.com/issues")
217 222

@@ -228,4 +233,8 @@ defmodule OpenAgentsWeb.IssueIndexLiveTest do

228 233
      live(build_conn(), ~p"/SecondOrg/hidden-repo/issues")
229 234
    end
230 235
  end
236
237
  defp repository do
238
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
239
  end
231 240
end
test/openagents_web/live/issue_new_live_test.exs modified +14 -10

@@ -8,7 +8,7 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

8 8
  alias OpenAgents.Issues
9 9
10 10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "issue-new")}
11
    {:ok, conn: log_in_repository_user(conn, "issue-new", repository())}
12 12
  end
13 13
14 14
  test "mounts with an empty form and a cancel link back to the list", %{conn: conn} do

@@ -22,8 +22,8 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

22 22
  end
23 23
24 24
  test "the milestone and label selects offer the seeded records", %{conn: conn} do
25
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
26
    label_fixture(%{name: "bug", color: "d73a4a"})
25
    milestone = milestone_fixture(repository(), %{title: "v1.0", due_on: nil})
26
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
27 27
28 28
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/new")
29 29

@@ -47,7 +47,7 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

47 47
      |> form("#new-issue-form", issue: %{title: "Add a runbook", body: "Please"})
48 48
      |> render_submit()
49 49
50
    assert [issue] = Issues.list_issues()
50
    assert [issue] = Issues.list_issues(repository())
51 51
    assert issue.title == "Add a runbook"
52 52
    assert issue.body == "Please"
53 53

@@ -59,8 +59,8 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

59 59
  end
60 60
61 61
  test "a submitted label and milestone are applied to the new issue", %{conn: conn} do
62
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
63
    label_fixture(%{name: "bug", color: "d73a4a"})
62
    milestone = milestone_fixture(repository(), %{title: "v1.0", due_on: nil})
63
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
64 64
65 65
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/new")
66 66

@@ -76,14 +76,14 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

76 76
             )
77 77
             |> render_submit()
78 78
79
    assert [issue] = Issues.list_issues()
79
    assert [issue] = Issues.list_issues(repository())
80 80
    assert [%{"name" => "bug", "color" => "d73a4a"}] = issue.labels
81 81
    assert issue.milestone["title"] == "v1.0"
82 82
    assert issue.milestone["number"] == milestone.number
83 83
  end
84 84
85 85
  test "an empty label and milestone selection leaves the issue bare", %{conn: conn} do
86
    label_fixture(%{name: "bug", color: "d73a4a"})
86
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
87 87
88 88
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/new")
89 89

@@ -94,7 +94,7 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

94 94
             )
95 95
             |> render_submit()
96 96
97
    assert [issue] = Issues.list_issues()
97
    assert [issue] = Issues.list_issues(repository())
98 98
    assert issue.labels == []
99 99
    assert issue.milestone == nil
100 100
  end

@@ -109,6 +109,10 @@ defmodule OpenAgentsWeb.IssueNewLiveTest do

109 109
110 110
    assert html =~ "can&#39;t be blank"
111 111
    assert has_element?(view, "#new-issue-form")
112
    assert Issues.list_issues(state: "all") == []
112
    assert Issues.list_issues(repository(), state: "all") == []
113
  end
114
115
  defp repository do
116
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
113 117
  end
114 118
end
test/openagents_web/live/issue_show_live_test.exs modified +31 -23

@@ -11,11 +11,11 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

11 11
  alias OpenAgents.Repositories
12 12
13 13
  setup %{conn: conn} do
14
    {:ok, conn: log_in_github_user(conn, "issue-show")}
14
    {:ok, conn: log_in_repository_user(conn, "issue-show", repository())}
15 15
  end
16 16
17 17
  defp issue!(attrs) do
18
    {:ok, issue} = Issues.create_issue(attrs)
18
    {:ok, issue} = Issues.create_issue(repository(), attrs)
19 19
    issue
20 20
  end
21 21

@@ -54,7 +54,8 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

54 54
  # an issue can never get its first label. So the group is always present and
55 55
  # states its own emptiness instead.
56 56
  test "an empty property group says so rather than disappearing", %{conn: conn} do
57
    repository_user_fixture("grace-show")
57
    grace = repository_user_fixture("grace-show")
58
    {:ok, _membership} = Repositories.add_member(repository(), grace, "contributor")
58 59
    bare = issue!(%{"title" => "Bare"})
59 60
    {:ok, view, html} = live(conn, path(bare))
60 61

@@ -71,8 +72,8 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

71 72
             "the #{id} property is stated but cannot be changed"
72 73
    end
73 74
74
    label_fixture(%{name: "bug", color: "d73a4a"})
75
    milestone = milestone_fixture(%{title: "v1.0", due_on: nil})
75
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
76
    milestone = milestone_fixture(repository(), %{title: "v1.0", due_on: nil})
76 77
77 78
    rich = issue!(%{"title" => "Rich", "labels" => ["bug"], "assignees" => ["grace-show"]})
78 79
    {:ok, rich} = Issues.set_milestone(rich, milestone.number)

@@ -86,9 +87,10 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

86 87
  end
87 88
88 89
  test "the rail changes state, labels, assignees, and the milestone", %{conn: conn} do
89
    repository_user_fixture("hopper-show")
90
    label_fixture(%{name: "bug", color: "d73a4a"})
91
    milestone = milestone_fixture(%{title: "v2.0", due_on: nil})
90
    hopper = repository_user_fixture("hopper-show")
91
    {:ok, _membership} = Repositories.add_member(repository(), hopper, "contributor")
92
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
93
    milestone = milestone_fixture(repository(), %{title: "v2.0", due_on: nil})
92 94
    issue = issue!(%{"title" => "Editable"})
93 95
94 96
    {:ok, view, _html} = live(conn, path(issue))

@@ -97,26 +99,28 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

97 99
    |> element(~s{#issue-label-menu button}, "bug")
98 100
    |> render_click()
99 101
100
    assert Issues.get_issue!(issue.id).labels |> Enum.map(& &1["name"]) == ["bug"]
102
    assert Issues.get_issue!(repository(), issue.id).labels |> Enum.map(& &1["name"]) == ["bug"]
101 103
102 104
    view
103 105
    |> element(~s{#issue-assignee-menu button}, "hopper-show")
104 106
    |> render_click()
105 107
106
    assert Issues.get_issue!(issue.id).assignees |> Enum.map(& &1["login"]) == ["hopper-show"]
108
    assert Issues.get_issue!(repository(), issue.id).assignees |> Enum.map(& &1["login"]) == [
109
             "hopper-show"
110
           ]
107 111
108 112
    view
109 113
    |> element(~s{#issue-milestone-menu button}, "v2.0")
110 114
    |> render_click()
111 115
112
    assert Issues.get_issue!(issue.id).milestone["number"] == milestone.number
116
    assert Issues.get_issue!(repository(), issue.id).milestone["number"] == milestone.number
113 117
114 118
    # The rail can pick a close reason the header's two buttons cannot.
115 119
    view
116 120
    |> element(~s{#issue-state-menu button}, "Closed as not planned")
117 121
    |> render_click()
118 122
119
    closed = Issues.get_issue!(issue.id)
123
    closed = Issues.get_issue!(repository(), issue.id)
120 124
    assert closed.state == "closed"
121 125
    assert closed.state_reason == "not_planned"
122 126

@@ -125,13 +129,13 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

125 129
    |> element(~s{#issue-label-menu button}, "bug")
126 130
    |> render_click()
127 131
128
    assert Issues.get_issue!(issue.id).labels == []
132
    assert Issues.get_issue!(repository(), issue.id).labels == []
129 133
  end
130 134
131 135
  test "a member who loses write access cannot triage through an open page", %{conn: conn} do
132 136
    user = Accounts.get_user(Plug.Conn.get_session(conn, "user_id"))
133 137
    issue = issue!(%{"title" => "Authority changed"})
134
    repository = Repositories.initial_repository!()
138
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
135 139
136 140
    {:ok, view, _html} = live(conn, path(issue))
137 141
    {:ok, _} = Repositories.add_member(repository, user, "viewer")

@@ -139,7 +143,7 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

139 143
    html = render_click(view, "toggle_label", %{"name" => "bug"})
140 144
141 145
    assert html =~ "Only repository members can change issue labels"
142
    assert Issues.get_issue!(issue.id).labels == []
146
    assert Issues.get_issue!(repository(), issue.id).labels == []
143 147
  end
144 148
145 149
  test "the history is one feed of comments and state changes", %{conn: conn} do

@@ -211,7 +215,7 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

211 215
    assert has_element?(view, ~s{button[phx-click="reopen"]}, "Reopen issue")
212 216
    refute has_element?(view, ~s{button[phx-click="close"]})
213 217
214
    closed = Issues.get_issue!(issue.id)
218
    closed = Issues.get_issue!(repository(), issue.id)
215 219
    assert closed.state == "closed"
216 220
    assert closed.state_reason == "completed"
217 221
    assert closed.closed_at

@@ -221,7 +225,7 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

221 225
    assert html =~ "Issue reopened"
222 226
    assert has_element?(view, ~s{button[phx-click="close"]}, "Close issue")
223 227
224
    reopened = Issues.get_issue!(issue.id)
228
    reopened = Issues.get_issue!(repository(), issue.id)
225 229
    assert reopened.state == "open"
226 230
    assert reopened.closed_at == nil
227 231
  end

@@ -257,7 +261,7 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

257 261
    assert html =~ "After"
258 262
    refute has_element?(view, "#issue-edit-form")
259 263
260
    updated = Issues.get_issue!(issue.id)
264
    updated = Issues.get_issue!(repository(), issue.id)
261 265
    assert updated.title == "Edited"
262 266
    assert updated.body == "After"
263 267
  end

@@ -275,7 +279,7 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

275 279
276 280
    assert html =~ "can&#39;t be blank"
277 281
    assert has_element?(view, "#issue-edit-form")
278
    assert Issues.get_issue!(issue.id).title == "Editable"
282
    assert Issues.get_issue!(repository(), issue.id).title == "Editable"
279 283
  end
280 284
281 285
  test "adding a comment appends it to the thread and bumps the count", %{conn: conn} do

@@ -294,9 +298,9 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

294 298
    assert html =~ "Looks good to me"
295 299
    assert html =~ "anonymous"
296 300
297
    assert [comment] = Issues.list_comments(issue.id)
301
    assert [comment] = Issues.list_comments(issue)
298 302
    assert comment.body == "Looks good to me"
299
    assert Issues.get_issue!(issue.id).comments == 1
303
    assert Issues.get_issue!(repository(), issue.id).comments == 1
300 304
  end
301 305
302 306
  test "an empty comment body is rejected and nothing is stored", %{conn: conn} do

@@ -309,8 +313,8 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

309 313
      |> render_submit()
310 314
311 315
    assert html =~ "can&#39;t be blank"
312
    assert Issues.list_comments(issue.id) == []
313
    assert Issues.get_issue!(issue.id).comments == 0
316
    assert Issues.list_comments(issue) == []
317
    assert Issues.get_issue!(repository(), issue.id).comments == 0
314 318
  end
315 319
316 320
  test "existing comments render with their author on mount", %{conn: conn} do

@@ -342,4 +346,8 @@ defmodule OpenAgentsWeb.IssueShowLiveTest do

342 346
    refute has_element?(view, ~s{button[phx-click="close"]})
343 347
    refute has_element?(view, ~s{button[phx-click="toggle_edit"]})
344 348
  end
349
350
  defp repository do
351
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
352
  end
345 353
end
test/openagents_web/live/label_index_live_test.exs modified +16 -7

@@ -7,7 +7,7 @@ defmodule OpenAgentsWeb.LabelIndexLiveTest do

7 7
  alias OpenAgents.Labels
8 8
9 9
  setup %{conn: conn} do
10
    {:ok, conn: log_in_github_user(conn, "label-index")}
10
    {:ok, conn: log_in_repository_user(conn, "label-index", repository())}
11 11
  end
12 12
13 13
  test "mounts with the create form and an empty state", %{conn: conn} do

@@ -20,8 +20,13 @@ defmodule OpenAgentsWeb.LabelIndexLiveTest do

20 20
  end
21 21
22 22
  test "lists seeded labels with their descriptions", %{conn: conn} do
23
    label_fixture(%{name: "bug", color: "d73a4a", description: "Something is broken"})
24
    label_fixture(%{name: "docs", color: "0075ca", description: nil})
23
    label_fixture(repository(), %{
24
      name: "bug",
25
      color: "d73a4a",
26
      description: "Something is broken"
27
    })
28
29
    label_fixture(repository(), %{name: "docs", color: "0075ca", description: nil})
25 30
26 31
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/labels")
27 32

@@ -44,7 +49,7 @@ defmodule OpenAgentsWeb.LabelIndexLiveTest do

44 49
45 50
    assert html =~ "Label created"
46 51
    assert has_element?(view, "#labels td", "enhancement")
47
    assert [%Labels.Label{name: "enhancement"}] = Labels.list_labels()
52
    assert [%Labels.Label{name: "enhancement"}] = Labels.list_labels(repository())
48 53
  end
49 54
50 55
  test "a label missing its required color re-renders the form with an error", %{conn: conn} do

@@ -57,11 +62,11 @@ defmodule OpenAgentsWeb.LabelIndexLiveTest do

57 62
58 63
    assert html =~ "can&#39;t be blank"
59 64
    assert has_element?(view, "#new-label-form")
60
    assert Labels.list_labels() == []
65
    assert Labels.list_labels(repository()) == []
61 66
  end
62 67
63 68
  test "deleting a label removes it and returns the empty state", %{conn: conn} do
64
    label = label_fixture(%{name: "wontfix", color: "ffffff"})
69
    label = label_fixture(repository(), %{name: "wontfix", color: "ffffff"})
65 70
66 71
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/labels")
67 72
    assert has_element?(view, "#labels td", "wontfix")

@@ -73,6 +78,10 @@ defmodule OpenAgentsWeb.LabelIndexLiveTest do

73 78
74 79
    assert html =~ "Label deleted"
75 80
    assert has_element?(view, ~s{[role="status"]}, "No labels yet")
76
    assert Labels.list_labels() == []
81
    assert Labels.list_labels(repository()) == []
82
  end
83
84
  defp repository do
85
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
77 86
  end
78 87
end
test/openagents_web/live/member_index_live_test.exs modified +38 -8

@@ -21,13 +21,27 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

21 21
22 22
  defp sign_in_owner(conn, key) do
23 23
    user = repository_user_fixture(key)
24
    {:ok, _} = Repositories.add_member(Repositories.initial_repository!(), user, "owner")
24
25
    {:ok, _} =
26
      Repositories.add_member(
27
        Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
28
        user,
29
        "owner"
30
      )
31
25 32
    {Plug.Test.init_test_session(conn, %{"user_id" => user.id}), user}
26 33
  end
27 34
28 35
  test "an owner sees the members and their roles", %{conn: conn} do
29 36
    {conn, _owner} = sign_in_owner(conn, "members-view-owner")
30
    repository_user_fixture("members-view-member")
37
    member = repository_user_fixture("members-view-member")
38
39
    {:ok, _membership} =
40
      Repositories.add_member(
41
        Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
42
        member,
43
        "contributor"
44
      )
31 45
32 46
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/members")
33 47

@@ -50,7 +64,10 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

50 64
    assert has_element?(view, "#members")
51 65
    assert html =~ "recruit"
52 66
53
    assert Repositories.membership_role(Repositories.initial_repository!(), recruit) ==
67
    assert Repositories.membership_role(
68
             Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
69
             recruit
70
           ) ==
54 71
             "maintainer"
55 72
  end
56 73

@@ -70,6 +87,8 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

70 87
  test "an owner changes a member's role", %{conn: conn} do
71 88
    {conn, _owner} = sign_in_owner(conn, "members-role-owner")
72 89
    member = repository_user_fixture("role-target")
90
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
91
    {:ok, _membership} = Repositories.add_member(repository, member, "contributor")
73 92
74 93
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/members")
75 94

@@ -80,14 +99,18 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

80 99
    })
81 100
    |> render_change()
82 101
83
    assert Repositories.membership_role(Repositories.initial_repository!(), member) ==
102
    assert Repositories.membership_role(
103
             Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
104
             member
105
           ) ==
84 106
             "maintainer"
85 107
  end
86 108
87 109
  test "an owner removes a member", %{conn: conn} do
88 110
    {conn, _owner} = sign_in_owner(conn, "members-remove-owner")
89 111
    member = repository_user_fixture("remove-target")
90
    repository = Repositories.initial_repository!()
112
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
113
    {:ok, _membership} = Repositories.add_member(repository, member, "contributor")
91 114
92 115
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/members")
93 116

@@ -101,7 +124,7 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

101 124
  test "a demoted owner cannot keep administering through an open page", %{conn: conn} do
102 125
    {conn, owner} = sign_in_owner(conn, "members-stale-owner")
103 126
    recruit = plain_user("members-stale-recruit")
104
    repository = Repositories.initial_repository!()
127
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
105 128
106 129
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/members")
107 130
    {:ok, _} = Repositories.add_member(repository, owner, "contributor")

@@ -126,11 +149,18 @@ defmodule OpenAgentsWeb.MemberIndexLiveTest do

126 149
      |> render_click()
127 150
128 151
    assert html =~ "at least one owner"
129
    assert Repositories.member?(Repositories.initial_repository!(), owner)
152
153
    assert Repositories.member?(
154
             Repositories.get_by_path!("OpenAgentsInc", "openagents.com"),
155
             owner
156
           )
130 157
  end
131 158
132 159
  test "a non-owner is sent away", %{conn: conn} do
133
    conn = log_in_github_user(conn, "plain-contributor")
160
    user = repository_user_fixture("plain-contributor")
161
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
162
    {:ok, _membership} = Repositories.add_member(repository, user, "contributor")
163
    conn = Plug.Test.init_test_session(conn, %{"user_id" => user.id})
134 164
135 165
    {:error, {:redirect, %{to: to}}} = live(conn, ~p"/OpenAgentsInc/openagents.com/members")
136 166
test/openagents_web/live/milestone_index_live_test.exs modified +23 -12

@@ -8,7 +8,7 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

8 8
  alias OpenAgents.Milestones
9 9
10 10
  setup %{conn: conn} do
11
    {:ok, conn: log_in_github_user(conn, "milestone-index")}
11
    {:ok, conn: log_in_repository_user(conn, "milestone-index", repository())}
12 12
  end
13 13
14 14
  test "mounts with the create form and an empty state", %{conn: conn} do

@@ -20,7 +20,12 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

20 20
  end
21 21
22 22
  test "lists seeded milestones with their state and due date", %{conn: conn} do
23
    milestone_fixture(%{title: "v1.0", state: "open", due_on: "2026-12-31", description: "Ship"})
23
    milestone_fixture(repository(), %{
24
      title: "v1.0",
25
      state: "open",
26
      due_on: "2026-12-31",
27
      description: "Ship"
28
    })
24 29
25 30
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/milestones")
26 31

@@ -31,12 +36,12 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

31 36
  end
32 37
33 38
  test "the progress meter reports the closed-to-total ratio", %{conn: conn} do
34
    milestone = milestone_fixture(%{title: "Beta", state: "open", due_on: nil})
39
    milestone = milestone_fixture(repository(), %{title: "Beta", state: "open", due_on: nil})
35 40
36
    {:ok, open_issue} = Issues.create_issue(%{"title" => "Still open"})
41
    {:ok, open_issue} = Issues.create_issue(repository(), %{"title" => "Still open"})
37 42
    {:ok, _} = Issues.set_milestone(open_issue, milestone.number)
38 43
39
    {:ok, done_issue} = Issues.create_issue(%{"title" => "Finished"})
44
    {:ok, done_issue} = Issues.create_issue(repository(), %{"title" => "Finished"})
40 45
    {:ok, done_issue} = Issues.set_milestone(done_issue, milestone.number)
41 46
    {:ok, _} = Issues.update_issue(done_issue, %{"state" => "closed"})
42 47

@@ -54,7 +59,7 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

54 59
  end
55 60
56 61
  test "a milestone with no issues reports zero progress", %{conn: conn} do
57
    milestone_fixture(%{title: "Empty", due_on: nil})
62
    milestone_fixture(repository(), %{title: "Empty", due_on: nil})
58 63
59 64
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/milestones")
60 65

@@ -76,7 +81,9 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

76 81
77 82
    assert html =~ "Milestone created"
78 83
    assert html =~ "v2.0"
79
    assert [%Milestones.Milestone{title: "v2.0", number: 1}] = Milestones.list_milestones()
84
85
    assert [%Milestones.Milestone{title: "v2.0", number: 1}] =
86
             Milestones.list_milestones(repository())
80 87
  end
81 88
82 89
  test "a milestone with no title re-renders the form with an error", %{conn: conn} do

@@ -88,11 +95,11 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

88 95
      |> render_submit()
89 96
90 97
    assert html =~ "can&#39;t be blank"
91
    assert Milestones.list_milestones() == []
98
    assert Milestones.list_milestones(repository()) == []
92 99
  end
93 100
94 101
  test "closing a milestone flips its state and hides the close button", %{conn: conn} do
95
    milestone = milestone_fixture(%{title: "Closeable", state: "open", due_on: nil})
102
    milestone = milestone_fixture(repository(), %{title: "Closeable", state: "open", due_on: nil})
96 103
97 104
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/milestones")
98 105
    assert has_element?(view, ~s{button[phx-click="close"][phx-value-id="#{milestone.id}"]})

@@ -104,11 +111,11 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

104 111
105 112
    assert html =~ "Milestone closed"
106 113
    refute has_element?(view, ~s{button[phx-click="close"][phx-value-id="#{milestone.id}"]})
107
    assert Milestones.get_milestone!(milestone.id).state == "closed"
114
    assert Milestones.get_milestone!(repository(), milestone.id).state == "closed"
108 115
  end
109 116
110 117
  test "deleting a milestone returns the empty state", %{conn: conn} do
111
    milestone = milestone_fixture(%{title: "Deletable", due_on: nil})
118
    milestone = milestone_fixture(repository(), %{title: "Deletable", due_on: nil})
112 119
113 120
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/milestones")
114 121

@@ -119,6 +126,10 @@ defmodule OpenAgentsWeb.MilestoneIndexLiveTest do

119 126
120 127
    assert html =~ "Milestone deleted"
121 128
    assert has_element?(view, ~s{[role="status"]}, "No milestones yet")
122
    assert Milestones.list_milestones() == []
129
    assert Milestones.list_milestones(repository()) == []
130
  end
131
132
  defp repository do
133
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
123 134
  end
124 135
end
test/openagents_web/live/project_index_live_test.exs modified +16 -8

@@ -8,6 +8,7 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

8 8
9 9
  setup %{conn: conn} do
10 10
    user = github_user("project-index")
11
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "owner")
11 12
    conn = Plug.Test.init_test_session(conn, %{"user_id" => user.id})
12 13
    {:ok, conn: conn, user: user}
13 14
  end

@@ -21,7 +22,8 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

21 22
  end
22 23
23 24
  test "lists projects owned by the URL owner and links to each board", %{conn: conn} do
24
    project = project_fixture(%{title: "Roadmap", owner: "OpenAgentsInc", state: "open"})
25
    project =
26
      project_fixture(repository(), %{title: "Roadmap", owner: "OpenAgentsInc", state: "open"})
25 27
26 28
    {:ok, view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/projects")
27 29

@@ -45,7 +47,7 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

45 47
  end
46 48
47 49
  test "the repository board lists projects regardless of their user owner", %{conn: conn} do
48
    project_fixture(%{title: "Someone elses", owner: "other-org"})
50
    project_fixture(repository(), %{title: "Someone elses", owner: "other-org"})
49 51
50 52
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/projects")
51 53

@@ -67,7 +69,7 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

67 69
    assert html =~ "Project created"
68 70
    assert html =~ "Q3 delivery"
69 71
70
    assert [project] = Projects.list_projects()
72
    assert [project] = Projects.list_projects(repository())
71 73
    assert project.title == "Q3 delivery"
72 74
    assert project.owner == user.github_login
73 75
    assert project.state == "open"

@@ -83,11 +85,11 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

83 85
84 86
    assert html =~ "can&#39;t be blank"
85 87
    assert has_element?(view, "#new-project-form")
86
    assert Projects.list_projects() == []
88
    assert Projects.list_projects(repository()) == []
87 89
  end
88 90
89 91
  test "deleting a project returns the empty state", %{conn: conn} do
90
    project = project_fixture(%{title: "Doomed", owner: "OpenAgentsInc"})
92
    project = project_fixture(repository(), %{title: "Doomed", owner: "OpenAgentsInc"})
91 93
92 94
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/projects")
93 95

@@ -98,20 +100,22 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

98 100
99 101
    assert html =~ "Project deleted"
100 102
    assert has_element?(view, ~s{[role="status"]}, "No projects yet")
101
    assert Projects.list_projects() == []
103
    assert Projects.list_projects(repository()) == []
102 104
  end
103 105
104 106
  test "closing a project from its row uses the state GitHub already has", %{conn: conn} do
105 107
    # Projects V2 carries `state`, so this changes a real field rather than an
106 108
    # invented one. Delete stays a separate control beside the row.
107
    project = project_fixture(%{title: "Closeable", owner: "OpenAgentsInc", state: "open"})
109
    project =
110
      project_fixture(repository(), %{title: "Closeable", owner: "OpenAgentsInc", state: "open"})
111
108 112
    {:ok, view, _html} = live(conn, ~p"/OpenAgentsInc/openagents.com/projects")
109 113
110 114
    view
111 115
    |> element(~s{#project-state-#{project.id} button}, "Closed")
112 116
    |> render_click()
113 117
114
    assert Projects.get_project!(project.id).state == "closed"
118
    assert Projects.get_project!(repository(), project.id).state == "closed"
115 119
116 120
    assert has_element?(
117 121
             view,

@@ -119,4 +123,8 @@ defmodule OpenAgentsWeb.ProjectIndexLiveTest do

119 123
             "Closed"
120 124
           )
121 125
  end
126
127
  defp repository do
128
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
129
  end
122 130
end
test/openagents_web/live/project_show_live_test.exs modified +19 -13

@@ -10,10 +10,10 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

10 10
  alias OpenAgents.Projects
11 11
12 12
  setup %{conn: conn} do
13
    {:ok, conn: log_in_github_user(conn, "project-show")}
13
    {:ok, conn: log_in_repository_user(conn, "project-show", repository())}
14 14
  end
15 15
16
  defp project!, do: project_fixture(%{title: "Roadmap", owner: "OpenAgentsInc"})
16
  defp project!, do: project_fixture(repository(), %{title: "Roadmap", owner: "OpenAgentsInc"})
17 17
18 18
  defp path(project), do: ~p"/OpenAgentsInc/openagents.com/projects/#{project.number}"
19 19

@@ -47,8 +47,8 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

47 47
48 48
  test "the issue select offers every issue, open or closed", %{conn: conn} do
49 49
    project = project!()
50
    {:ok, open} = Issues.create_issue(%{"title" => "Still open"})
51
    {:ok, closed} = Issues.create_issue(%{"title" => "All done"})
50
    {:ok, open} = Issues.create_issue(repository(), %{"title" => "Still open"})
51
    {:ok, closed} = Issues.create_issue(repository(), %{"title" => "All done"})
52 52
    {:ok, _} = Issues.update_issue(closed, %{"state" => "closed"})
53 53
54 54
    {:ok, view, _html} = live(conn, path(project))

@@ -72,13 +72,15 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

72 72
73 73
  test "an existing item renders as a card in its status column", %{conn: conn} do
74 74
    project = project!()
75
    label_fixture(%{name: "bug", color: "d73a4a"})
76
    {:ok, issue} = Issues.create_issue(%{"title" => "Fix the parser", "labels" => ["bug"]})
75
    label_fixture(repository(), %{name: "bug", color: "d73a4a"})
76
77
    {:ok, issue} =
78
      Issues.create_issue(repository(), %{"title" => "Fix the parser", "labels" => ["bug"]})
77 79
78 80
    {:ok, _item} =
79 81
      Projects.create_project_item(
80 82
        %{"issue_number" => issue.number, "values" => %{"Status" => "In Progress"}},
81
        project.id
83
        project
82 84
      )
83 85
84 86
    {:ok, view, html} = live(conn, path(project))

@@ -95,12 +97,12 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

95 97
96 98
  test "an item with no recorded status falls back to To Do", %{conn: conn} do
97 99
    project = project!()
98
    {:ok, issue} = Issues.create_issue(%{"title" => "Unsorted"})
100
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Unsorted"})
99 101
100 102
    {:ok, _item} =
101 103
      Projects.create_project_item(
102 104
        %{"issue_number" => issue.number, "values" => %{}},
103
        project.id
105
        project
104 106
      )
105 107
106 108
    {:ok, _view, html} = live(conn, path(project))

@@ -110,7 +112,7 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

110 112
111 113
  test "submitting the form adds the issue to the board", %{conn: conn} do
112 114
    project = project!()
113
    {:ok, issue} = Issues.create_issue(%{"title" => "Ship the runbook"})
115
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Ship the runbook"})
114 116
115 117
    {:ok, view, html} = live(conn, path(project))
116 118
    refute html =~ "Ship the runbook</a>"

@@ -130,17 +132,17 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

130 132
             "Ship the runbook"
131 133
           )
132 134
133
    assert [item] = Projects.list_project_items(project.id)
135
    assert [item] = Projects.list_project_items(project)
134 136
    assert item.issue_id == issue.id
135 137
    assert item.values == %{"Status" => "Done"}
136 138
  end
137 139
138 140
  test "a fixture-built item lands in the column named by its Status value", %{conn: conn} do
139 141
    project = project!()
140
    {:ok, issue} = Issues.create_issue(%{"title" => "Fixture-placed"})
142
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Fixture-placed"})
141 143
142 144
    item =
143
      project_item_fixture(%{
145
      project_item_fixture(repository(), %{
144 146
        project_id: project.id,
145 147
        issue_id: issue.id,
146 148
        values: %{"Status" => "Done"}

@@ -168,4 +170,8 @@ defmodule OpenAgentsWeb.ProjectShowLiveTest do

168 170
      live(conn, ~p"/OpenAgentsInc/openagents.com/projects/9999")
169 171
    end
170 172
  end
173
174
  defp repository do
175
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
176
  end
171 177
end
test/openagents_web/route_authority_test.exs modified +3 -1

@@ -28,6 +28,8 @@ defmodule OpenAgentsWeb.RouteAuthorityTest do

28 28
    write = route!(:post, "/api/v3/repos/:owner/:repo/issues")
29 29
30 30
    assert read.class == :public_read
31
    assert read.principal == "anonymous or first-party bearer token"
32
    assert read.scope == "forge:repository:read"
31 33
    assert read.mutation == false
32 34
    assert write.class == :authenticated_api
33 35
    assert write.principal == "first-party bearer token"

@@ -39,7 +41,7 @@ defmodule OpenAgentsWeb.RouteAuthorityTest do

39 41
             "GET",
40 42
             "/api/v3/repos/OpenAgentsInc/openagents.com/issues",
41 43
             "stage.openagents.com"
42
           ).pipe_through == [:api]
44
           ).pipe_through == [:optional_forge_api]
43 45
44 46
    assert Phoenix.Router.route_info(
45 47
             OpenAgentsWeb.Router,
test/support/conn_case.ex modified +21 -2

@@ -28,6 +28,7 @@ defmodule OpenAgentsWeb.ConnCase do

28 28
      import Plug.Conn
29 29
      import Phoenix.ConnTest
30 30
      import OpenAgentsWeb.ConnCase
31
      import OpenAgents.AccountsFixtures
31 32
    end
32 33
  end
33 34

@@ -54,8 +55,6 @@ defmodule OpenAgentsWeb.ConnCase do

54 55
        github_avatar_url: "https://avatars.githubusercontent.com/u/#{github_id}?v=4"
55 56
      })
56 57
57
    {:ok, _membership} = OpenAgents.Repositories.ensure_initial_membership(user)
58
59 58
    user
60 59
  end
61 60

@@ -64,6 +63,12 @@ defmodule OpenAgentsWeb.ConnCase do

64 63
    Plug.Test.init_test_session(conn, %{"user_id" => user.id})
65 64
  end
66 65
66
  def log_in_repository_user(conn, key, repository) when is_binary(key) do
67
    user = github_user(key)
68
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, "owner")
69
    Plug.Test.init_test_session(conn, %{"user_id" => user.id})
70
  end
71
67 72
  @doc """
68 73
  Logs in an account that has already written to the agent.
69 74

@@ -85,6 +90,13 @@ defmodule OpenAgentsWeb.ConnCase do

85 90
    put_forge_api_token_for_user(conn, user)
86 91
  end
87 92
93
  def put_forge_api_token(conn, key, %OpenAgents.Repositories.Repository{} = repository)
94
      when is_binary(key) do
95
    user = github_user("api-token-" <> key)
96
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, "owner")
97
    put_forge_api_token_for_user(conn, user)
98
  end
99
88 100
  def put_forge_api_token(conn, key, login)
89 101
      when is_binary(key) and is_binary(login) do
90 102
    user = github_user("api-token-" <> key, login)

@@ -92,6 +104,13 @@ defmodule OpenAgentsWeb.ConnCase do

92 104
    put_forge_api_token_for_user(conn, user)
93 105
  end
94 106
107
  def put_forge_api_token(conn, key, login, %OpenAgents.Repositories.Repository{} = repository)
108
      when is_binary(key) and is_binary(login) do
109
    user = github_user("api-token-" <> key, login)
110
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, "owner")
111
    put_forge_api_token_for_user(conn, user)
112
  end
113
95 114
  defp put_forge_api_token_for_user(conn, user) do
96 115
    {:ok, _credential, plaintext} =
97 116
      OpenAgents.ApiTokens.create(user, %{
test/support/data_case.ex modified +1

@@ -24,6 +24,7 @@ defmodule OpenAgents.DataCase do

24 24
      import Ecto.Changeset
25 25
      import Ecto.Query
26 26
      import OpenAgents.DataCase
27
      import OpenAgents.AccountsFixtures
27 28
    end
28 29
  end
29 30
test/support/fixtures/accounts_fixtures.ex modified +24 -1

@@ -11,7 +11,30 @@ defmodule OpenAgents.AccountsFixtures do

11 11
        github_avatar_url: "https://avatars.githubusercontent.com/u/#{github_id}?v=4"
12 12
      })
13 13
14
    {:ok, _membership} = OpenAgents.Repositories.ensure_initial_membership(user)
15 14
    user
16 15
  end
16
17
  def repository_fixture(attrs \\ %{}) do
18
    suffix = System.unique_integer([:positive, :monotonic])
19
20
    defaults = %{
21
      owner: "TestOrg#{suffix}",
22
      name: "test-repository-#{suffix}",
23
      visibility: "public",
24
      default_branch: "main"
25
    }
26
27
    {:ok, repository} =
28
      defaults
29
      |> Map.merge(Map.new(attrs))
30
      |> OpenAgents.Repositories.create_repository()
31
32
    repository
33
  end
34
35
  def repository_with_member_fixture(user, attrs \\ %{}, role \\ "owner") do
36
    repository = repository_fixture(attrs)
37
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, role)
38
    repository
39
  end
17 40
end
test/support/fixtures/issues_fixtures.ex modified +2 -4

@@ -10,11 +10,9 @@ defmodule OpenAgents.IssuesFixtures do

10 10
  The issue number is assigned by the context, so callers get a fresh,
11 11
  monotonically increasing number without having to coordinate.
12 12
  """
13
  def issue_fixture(attrs \\ %{}) do
13
  def issue_fixture(repository, attrs \\ %{}) do
14 14
    {:ok, issue} =
15
      attrs
16
      |> Enum.into(%{title: "some title"})
17
      |> OpenAgents.Issues.create_issue()
15
      OpenAgents.Issues.create_issue(repository, Enum.into(attrs, %{title: "some title"}))
18 16
19 17
    issue
20 18
  end
test/support/fixtures/labels_fixtures.ex modified +9 -8

@@ -7,15 +7,16 @@ defmodule OpenAgents.LabelsFixtures do

7 7
  @doc """
8 8
  Generate a label.
9 9
  """
10
  def label_fixture(attrs \\ %{}) do
10
  def label_fixture(repository, attrs \\ %{}) do
11 11
    {:ok, label} =
12
      attrs
13
      |> Enum.into(%{
14
        color: "some color",
15
        description: "some description",
16
        name: "some name"
17
      })
18
      |> OpenAgents.Labels.create_label()
12
      OpenAgents.Labels.create_label(
13
        repository,
14
        Enum.into(attrs, %{
15
          color: "some color",
16
          description: "some description",
17
          name: "some name"
18
        })
19
      )
19 20
20 21
    label
21 22
  end
test/support/fixtures/milestones_fixtures.ex modified +11 -10

@@ -7,17 +7,18 @@ defmodule OpenAgents.MilestonesFixtures do

7 7
  @doc """
8 8
  Generate a milestone.
9 9
  """
10
  def milestone_fixture(attrs \\ %{}) do
10
  def milestone_fixture(repository, attrs \\ %{}) do
11 11
    {:ok, milestone} =
12
      attrs
13
      |> Enum.into(%{
14
        description: "some description",
15
        due_on: "some due_on",
16
        number: 42,
17
        state: "some state",
18
        title: "some title"
19
      })
20
      |> OpenAgents.Milestones.create_milestone()
12
      OpenAgents.Milestones.create_milestone(
13
        repository,
14
        Enum.into(attrs, %{
15
          description: "some description",
16
          due_on: "some due_on",
17
          number: 42,
18
          state: "some state",
19
          title: "some title"
20
        })
21
      )
21 22
22 23
    milestone
23 24
  end
test/support/fixtures/project_fields_fixtures.ex modified +16 -3

@@ -7,9 +7,22 @@ defmodule OpenAgents.ProjectFieldsFixtures do

7 7
  @doc """
8 8
  Generate a project_field.
9 9
  """
10
  def project_field_fixture(attrs \\ %{}) do
11
    {:ok, project} =
12
      OpenAgents.Projects.create_project(%{title: "Fixture project", owner: "OpenAgents"})
10
  def project_field_fixture(repository, attrs \\ %{}) do
11
    normalized = Map.new(attrs)
12
    project_id = Map.get(normalized, :project_id) || Map.get(normalized, "project_id")
13
14
    project =
15
      if project_id do
16
        OpenAgents.Projects.get_project!(repository, project_id)
17
      else
18
        {:ok, project} =
19
          OpenAgents.Projects.create_project(repository, %{
20
            title: "Fixture project",
21
            owner: "OpenAgents"
22
          })
23
24
        project
25
      end
13 26
14 27
    {:ok, project_field} =
15 28
      attrs
test/support/fixtures/project_items_fixtures.ex modified +25 -5

@@ -7,11 +7,31 @@ defmodule OpenAgents.ProjectItemsFixtures do

7 7
  @doc """
8 8
  Generate a project_item.
9 9
  """
10
  def project_item_fixture(attrs \\ %{}) do
11
    {:ok, project} =
12
      OpenAgents.Projects.create_project(%{title: "Fixture project", owner: "OpenAgents"})
10
  def project_item_fixture(repository, attrs \\ %{}) do
11
    normalized = Map.new(attrs)
12
    project_id = Map.get(normalized, :project_id) || Map.get(normalized, "project_id")
13
    issue_id = Map.get(normalized, :issue_id) || Map.get(normalized, "issue_id")
13 14
14
    {:ok, issue} = OpenAgents.Issues.create_issue(%{title: "Fixture issue"})
15
    project =
16
      if project_id do
17
        OpenAgents.Projects.get_project!(repository, project_id)
18
      else
19
        {:ok, project} =
20
          OpenAgents.Projects.create_project(repository, %{
21
            title: "Fixture project",
22
            owner: "OpenAgents"
23
          })
24
25
        project
26
      end
27
28
    issue =
29
      if issue_id do
30
        OpenAgents.Issues.get_issue!(repository, issue_id)
31
      else
32
        {:ok, issue} = OpenAgents.Issues.create_issue(repository, %{title: "Fixture issue"})
33
        issue
34
      end
15 35
16 36
    {:ok, project_item} =
17 37
      attrs

@@ -20,7 +40,7 @@ defmodule OpenAgents.ProjectItemsFixtures do

20 40
        project_id: project.id,
21 41
        issue_id: issue.id
22 42
      })
23
      |> OpenAgents.ProjectItems.create_project_item()
43
      |> then(&OpenAgents.ProjectItems.create_project_item(repository, &1))
24 44
25 45
    project_item
26 46
  end
test/support/fixtures/projects_fixtures.ex modified +9 -8

@@ -7,15 +7,16 @@ defmodule OpenAgents.ProjectsFixtures do

7 7
  @doc """
8 8
  Generate a project.
9 9
  """
10
  def project_fixture(attrs \\ %{}) do
10
  def project_fixture(repository, attrs \\ %{}) do
11 11
    {:ok, project} =
12
      attrs
13
      |> Enum.into(%{
14
        owner: "some owner",
15
        state: "some state",
16
        title: "some title"
17
      })
18
      |> OpenAgents.Projects.create_project()
12
      OpenAgents.Projects.create_project(
13
        repository,
14
        Enum.into(attrs, %{
15
          owner: "some owner",
16
          state: "some state",
17
          title: "some title"
18
        })
19
      )
19 20
20 21
    project
21 22
  end

This page updates live while a promote is in flight · changelog